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:
- If a different preference was set for this service contract our code would break
- 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.