HowTo: Create Extension Attributes in Magento 2


Extension attributes are one of those Magento 2 concepts that sound complicated but click the moment you use them once. They let you add data to an existing entity (an order, a customer, a product) without rewriting its model, and they travel nicely through the API, which is where they really shine.

In this example I will add a delivery_note value to the order entity and expose it through the order repository.

This post assumes that there is already a custom module created in app/code/Vendor/Module. If you need to know how to create a custom module, look at How to create a custom module for Magento 2.

Declare the attribute

The declaration goes in etc/extension_attributes.xml. You point at the interface of the entity you want to extend.

xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/extension_attributes.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
    <extension_attributes for="Magento\Sales\Api\Data\OrderInterface">
        <attribute code="delivery_note" type="string"/>
    </extension_attributes>
</config>

After running bin/magento setup:di:compile (or just clearing generated/) Magento creates the getter and setter for you: $order->getExtensionAttributes()->getDeliveryNote().

Populate it when the order is loaded

Declaring the attribute does not fill it, we have to do that ourselves. The cleanest place is a plugin on the order repository, so the value is there every time somebody loads an order, including through the API.

php
<?php
/** app/code/Vendor/Module/Plugin/OrderRepositoryPlugin.php */
namespace Vendor\Module\Plugin;

use Magento\Sales\Api\Data\OrderExtensionFactory;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderRepositoryInterface;

class OrderRepositoryPlugin
{
    /**
     * @var OrderExtensionFactory
     */
    private $extensionFactory;

    /**
     * @param OrderExtensionFactory $extensionFactory
     */
    public function __construct(OrderExtensionFactory $extensionFactory)
    {
        $this->extensionFactory = $extensionFactory;
    }

    /**
     * @param OrderRepositoryInterface $subject
     * @param OrderInterface           $order
     * @return OrderInterface
     */
    public function afterGet(OrderRepositoryInterface $subject, OrderInterface $order)
    {
        $extension = $order->getExtensionAttributes() ?: $this->extensionFactory->create();

        // In a real project you would read this from your own table.
        $extension->setDeliveryNote('Leave the parcel with the neighbour');

        $order->setExtensionAttributes($extension);

        return $order;
    }
}

And we wire the plugin in di.xml:

xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Sales\Api\OrderRepositoryInterface">
        <plugin name="vendor_module_order_delivery_note" type="Vendor\Module\Plugin\OrderRepositoryPlugin"/>
    </type>
</config>
Notice: If you also need to persist the value, add an afterSave (or a beforeSave) plugin that reads the extension attribute and writes it to your own table. The declaration only creates the container, the storage is up to you.

From now on, any consumer of the order repository (a REST call to GET /V1/orders/:id, a custom service, an observer) will find delivery_note sitting inside the extension attributes.

Hope this helps.

If you have any question or know a better way to achieve this, feel free to share in the comments below.