One of the cornerstones of Magento has always been the extensibility of the platform. Magento 2 is no different, but some of the approaches to customising Magento have changed substantially with the new platform. One of the goals Magento had for Magento 2 was to get the balance right between extensibility and stability - which really are at opposite sides of the spectrum.

To bring stability Magento 2 introduced Service Contracts. Extension Attributes on the other hand are used to allow for customisation of the strict Service Contracts. Read on to have a look of how they all relate to each other.

Service contracts

One of the most important aspects of Magento 2 are service contracts. At the most basic level they are sets of php interfaces that define certain elements of a module.

By creating interfaces we are defining a contract, that classes, which implements it, must follow. With a clear contract like this it’s easy to know what to expect from a class and to switch concrete implementations:

The Service Contract approach has a lot of benefits:

  • Easy to switch concrete implementation (this is done by setting a preference for the interface in the dependency injection configuration files etc/di.xml)
  • Consistency over versions
  • Clear public API of the classes
  • Easier unit testing

Adding new properties in M1

In M1 there was a common need to add information to quote, order, invoice etc. The easiest way to achieve that was to add fields to the corresponding database tables like sales_flat_quote and sales_flat_order and then populate this via fieldsets:

<global> 
    ... 
    <fieldsets> 
        <sales_convert_quote> 
            <your_special_attribute> 
                <to_order>*</to_order> 
            </your_special_attribute> 
        </sales_convert_quote> 
    </fieldsets> 
</global>

This basically converts a quote field to the order. This attribute would become part of the order class’s $_data property and you would be able to easily access it with $order->getYourSpecialAttribute().

M2 approach

As we mentioned in the beginning, Magento 2 favours the interface approach, so that there are no more magic accessor methods that we used to have in M1. Let’s take a look at the Service Contract for the Sales Order class:

<?php

namespace Magento\Sales\Api\Data;

interface OrderInterface extends \Magento\Framework\Api\ExtensibleDataInterface
{

...

    /**
     * Gets the grand total for the order.
     *
     * @return float Grand total.
     */
    public function getGrandTotal();

    /**
     * Gets the shipping amount for the order.
     *
     * @return float|null Shipping amount.
     */
    public function getShippingAmount();

...

    /**
     * Sets the grand total for the order.
     *
     * @param float $amount
     * @return $this
     */
    public function setGrandTotal($amount);

    /**
     * Sets the shipping amount for the order.
     *
     * @param float $amount
     * @return $this
     */
    public function setShippingAmount($amount);

...

    /**
     * Retrieve existing extension attributes object or create a new one.
     *
     * @return \Magento\Sales\Api\Data\OrderExtensionInterface|null
     */
    public function getExtensionAttributes();

    /**
     * Set an extension attributes object.
     *
     * @param \Magento\Sales\Api\Data\OrderExtensionInterface $extensionAttributes
     * @return $this
     */
    public function setExtensionAttributes(\Magento\Sales\Api\Data\OrderExtensionInterface $extensionAttributes);
}

Yes, it has a lot of getters and setters as entities are meant to be data transfer objects.

Adding new attributes

We can’t change the interface each time we want to add a new attribute as we would lose backwards compatibility. Our logic has to be in line with service contracts mentioned earlier.

Workaround

At this moment we still could circumvent the interface as most of the classes still extend \Magento\Catalog\Model\AbstractModel but we shouldn’t do it for more than one reason:

  1. If a different preference was set for this service contract our code would break
  2. Our change is not documented as it relies on magic

So how can we add new attributes in an unobtrusive way that works within the constraints of the interface?

Extension attributes

If you look at the end of the interface, there are these two methods:

    /**
     * Retrieve existing extension attributes object or create a new one.
     *
     * @return \Magento\Sales\Api\Data\OrderExtensionInterface|null
     */
    public function getExtensionAttributes();

    /**
     * Set an extension attributes object.
     *
     * @param \Magento\Sales\Api\Data\OrderExtensionInterface $extensionAttributes
     * @return $this
     */
    public function setExtensionAttributes(\Magento\Sales\Api\Data\OrderExtensionInterface $extensionAttributes);

Extension attributes (and also custom attributes) are Magento 2’s approach to providing stabilibity as well as customisability.

What are they?

They are basically containers for additional data that we want to add to our entities.

We’ll focus on extension attributes in this article, custom attributes are more used for situation where you also need some GUI for the attributes.

Every interface that extends \Magento\Framework\Api\ExtensibleDataInterface can be extended by extension attributes. The methods defined in ExtensibleDataInterface provide access to the objects that contain additional data.

XML definitions

The first thing that we need to do is to declare our extension attributes in one xml file:

<!-- etc/extension_attributes.xml -->
<?xml version="1.0"?>
<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="fooman_attribute" type="Fooman\Example\Api\Data\FoomanAttributeInterface" />
    </extension_attributes>
</config>

This means that we are extending our Sales Order Interface with an attribute. It's accessible via $order->getExtensionAttributes()->getFoomanAttribute().

New interface

The next thing that we should do is create an interface for our extension attribute itself:

<?php

namespace Fooman\Example\Api\Data;

interface FoomanAttributeInterface
{
    const VALUE = 'value';

    /**
     * Return value.
     *
     * @return string|null 
     */
    public function getValue();

    /**
     * Set value.
     *
     * @param string|null $value
     * @return $this
     */
    public function setValue($value);
}

We’ll keep it simple for now.

Concrete class

Let’s create a concrete class for the interface that we have just created:

<?php

namespace Fooman\Example\Model;

class FoomanAttribute implements \Fooman\Example\Api\Data\FoomanAttributeInterface
{
    /**
     * {@inheritdoc}
     */
    public function getValue()
    {
        return $this->getData(self::VALUE);
    }

    /**
     * {@inheritdoc}
     */
    public function setValue($value)
    {
        return $this->setData(self::VALUE, $value);
    }
}

How does it work?

The interface \Magento\Sales\Api\Data\OrderExtensionInterface is generated by Magento, either on the fly in development mode, or as part of the compilation process. The code generation happens via \Magento\Framework\Code\Generator for any interface which can't be loaded. It will read our xml configuration and create an interface which includes a getter and setter for our fooman_attribute.

Saving and retrieving

One thing to note, which is a huge difference to Magento 1, our extension attributes are not magically saved to the database or populated when the quote, order, etc is loaded from the database.

So we need to figure out a way to save and load new values. The best approach for this task are plugins.

Plugins

Plugins are an idea that comes from aspect oriented programming that aims to add additional behaviour to existing code by executing some code at certain points. In Magento 2 plugins allow us to execute code before, after or around any public method. So let’s create a plugin that is going to save and retrieve our new attributes:

<!-- etc/di.xml -->
<?xml version="1.0"?>
<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="save_fooman_attribute" type="Fooman\Example\Plugin\OrderSave"/>
        <plugin name="get_fooman_attribute" type="Fooman\Example\Plugin\OrderGet"/>
    </type>
</config>

As you can see we have attached OrderSave and OrderGet plugins to the core OrderRepositoryInterface.

We now have to implement a specific logic for these classes.

Order Save

For order save we need to figure out a way to save our new attribute somewhere to the database:

<?php

namespace Fooman\Example\Plugin;

use Magento\Framework\Exception\CouldNotSaveException;

class OrderSave
{

...

    public function afterSave(
        \Magento\Sales\Api\OrderRepositoryInterface $subject,
        \Magento\Sales\Api\Data\OrderInterface $resultOrder
    ) {
        $resultOrder = $this->saveFoomanAttribute($resultOrder);

        return $resultOrder;
    }

    private function saveFoomanAttribute(\Magento\Sales\Api\Data\OrderInterface $order)
    {
        $extensionAttributes = $order->getExtensionAttributes();
        if (
            null !== $extensionAttributes &&
            null !== $extensionAttributes->getFoomanAttribute()
        ) {
            $foomanAttributeValue = $extensionAttributes->getFoomanAttribute()->getValue();
            try {
                // The actual implementation of the repository is omitted
                // but it is where you would save to the database (or any other persistent storage)
                $this->foomanExampleRepository->save($order->getEntityId(), $foomanAttributeValue);
            } catch (\Exception $e) {
                throw new CouldNotSaveException(
                    __('Could not add attribute to order: "%1"', $e->getMessage()),
                    $e
                );
            }
        }
        return $order;
    }

Order Get

In the order get case we want to attach our attribute to the order entity once it’s loaded.

<?php

namespace Fooman\Example\Plugin;

class OrderGet
{

...  

    public function afterGet(
        \Magento\Sales\Api\OrderRepositoryInterface $subject,
        \Magento\Sales\Api\Data\OrderInterface $resultOrder
    ) {
        $resultOrder = $this->getFoomanAttribute($resultOrder);

        return $resultOrder;
    }

    private function getFoomanAttribute(\Magento\Sales\Api\Data\OrderInterface $order)
    {

        try {
            // The actual implementation of the repository is omitted
            // but it is where you would load your value from the database (or any other persistent storage)
            $foomanAttributeValue = $this->foomanExampleRepository->get($order->getEntityId());
        } catch (NoSuchEntityException $e) {
            return $order;
        }

        $extensionAttributes = $order->getExtensionAttributes();
        $orderExtension = $extensionAttributes ? $extensionAttributes : $this->orderExtensionFactory->create();
        $foomanAttribute = $this->foomanAttributeFactory->create();
        $foomanAttribute->setValue($foomanAttributeValue);
        $orderExtension->setFoomanAttribute($foomanAttribute);
        $order->setExtensionAttributes($orderExtension);

        return $order;
    }

Play well with others!

One important thing to note is that in order for Extension Attributes to work as designed we always need to query the entity for existing extension_attributes via $order->getExtensionAttributes() before using the extension attribute factory to create a new one. If we omit this check we will wipe out any previously set extension attributes on the entity.

Conclusion

Yes, Extension Attributes involve more effort in Magento 2 compared to Magento 1. So why should we adopt them? The changes introduced with Extension Attributes on Service Contracts are a step forward in terms of platform stability while maintaining extensibility (no more extension clashes on competing rewrites for the Sales/Order model). Additionally one of the benefits that you will get from using Extension Attributes is that your added attribute data is automatically available via an API request.

Our Surcharge extensions are our first extensions that make use of Extension Attributes. We'll keep learning more about them as we will use them more in future projects. We'll revisit our blog post as we do.

Dusan Lukic

Dusan Lukic

Developer at Fooman

Want to receive our monthly email with the best Magento developer tips, tricks and news? Join 7000+ other Magento developers