Adding custom attribute to Magento 2 API response

Magento 2 implements the “service contracts” design pattern, which comprises a set of PHP interfaces defined for a module. A service contract encompasses both service and data interfaces. These interfaces conceal the intricacies of business logic from service requestors like controllers, web services, and other modules.

Magento 2 API is bound to service contracts via configuration files. The API Data interfaces strictly define the data set available for data object for the entity, return types etc. Basically, the data interface represents the data to be included in the response, when it comes to WebAPI. Third-party developers cannot change API Data interface in the Magento core. So, the only way to affect interfaces using configuration is to add extension attributes. However, there is one requirement – data object to be extended must also implement the Magento\Framework\Api\ExtensibleDataInterface interface.

Let’s imagine we need to add a new “Customer Feedback” field to order entity. The setup script is the following:

<?php
/* File: app/code/Atwix/OrderFeedback/Setup/InstallSchema.php */

namespace Atwix\OrderFeedback\Setup;

use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

/**
 * Class InstallSchema
 */
class InstallSchema implements InstallSchemaInterface
{
    /**
     * Custom order column
     */
    const ORDER_FEEDBACK_FIELD = 'customer_feedback';

    /**
     * @inheritdoc
     */
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $setup->startSetup();
        $setup->getConnection()->addColumn(
            $setup->getTable('sales_order'),
            self::ORDER_FEEDBACK_FIELD,
            [
                'type' => Table::TYPE_TEXT,
                'size' => 255,
                'nullable' => true,
                'comment' => 'Customer Feedback'
            ]
        );
        $setup->endSetup();
    }
}

However, this custom data will not be included in Get Order Data API response, because it’s not defined in Magento\Sales\Api\Data\OrderInterface interface. Thus, we need to define our customer_feedback extension attribute for the order extensible data object. And the configuration file of Magento module responsible for it is extension_attributes.xml.

<?xml version="1.0"?>
<!-- File: app/code/Atwix/OrderFeedback/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="customer_feedback" type="string" />
    </extension_attributes>
</config>

Using this action we will define the additional setCustomerFeedback and getCustomerFeedback for auto-generated Magento\Sales\Api\Data\OrderExtension class. So don’t forget to update the auto-generated files of your Magento instance.

Now we need to manually add the custom field value during the order data loading. The better way is to add the Magento 2 plugin for get and getList methods of repository class. The plugin declaration is the following:

<?xml version="1.0"?>
<!-- File: app/code/Atwix/OrderFeedback/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="atwix_orderfeedback_add_order_extension_attribute"
                type="Atwix\OrderFeedback\Plugin\OrderRepositoryPlugin" />
    </type>
</config>

The afterGet and afterGetList methods will be called after the corresponding repository methods execution. So, this way we can affect the results:

<?php
/* File: app/code/Atwix/OrderFeedback/Plugin/OrderRepositoryPlugin.php */

namespace Atwix\OrderFeedback\Plugin;

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

/**
 * Class OrderRepositoryPlugin
 */
class OrderRepositoryPlugin
{
    /**
     * Order feedback field name
     */
    const FIELD_NAME = 'customer_feedback';

    /**
     * Order Extension Attributes Factory
     *
     * @var OrderExtensionFactory
     */
    protected $extensionFactory;

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

    /**
     * Add "customer_feedback" extension attribute to order data object to make it accessible in API data
     *
     * @param OrderRepositoryInterface $subject
     * @param OrderInterface $order
     *
     * @return OrderInterface
     */
    public function afterGet(OrderRepositoryInterface $subject, OrderInterface $order)
    {
        $customerFeedback = $order->getData(self::FIELD_NAME);
        $extensionAttributes = $order->getExtensionAttributes();
        $extensionAttributes = $extensionAttributes ? $extensionAttributes : $this->extensionFactory->create();
        $extensionAttributes->setCustomerFeedback($customerFeedback);
        $order->setExtensionAttributes($extensionAttributes);

        return $order;
    }

    /**
     * Add "customer_feedback" extension attribute to order data object to make it accessible in Magento API data
     *
     * @param OrderRepositoryInterface $subject
     * @param OrderSearchResultInterface $searchResult
     *
     * @return OrderSearchResultInterface
     */
    public function afterGetList(OrderRepositoryInterface $subject, OrderSearchResultInterface $searchResult)
    {
        $orders = $searchResult->getItems();

        foreach ($orders as &$order) {
            $customerFeedback = $order->getData(self::FIELD_NAME);
            $extensionAttributes = $order->getExtensionAttributes();
            $extensionAttributes = $extensionAttributes ? $extensionAttributes : $this->extensionFactory->create();
            $extensionAttributes->setCustomerFeedback($customerFeedback);
            $order->setExtensionAttributes($extensionAttributes);
        }

        return $searchResult;
    }
}

Once the order entity is loaded, the Customer Feedback value will be added to the extension attributes data object. As you may have noticed, we also need to check whether the data object already contains the extension attributes object. Otherwise, we need to instantiate it using the Extension Factory.

Thanks for reading!