How to return anything and everything with Magento 2 API

Wahid NoryWahid Nory
5 min read

Introduction

Let's setup a use case where you need something to be added into a Magento 2 API. For example an extra string where some reference is stored for an order or something more complex with an object/array.

Plugin

To return data attached to extension_attributes you can use a plugin to do this. This is an example of an after plugin which sits on a searchCriteria for orders.

di.xml

<type name="\Magento\Sales\Api\OrderRepositoryInterface">
  <plugin name="Something_Awesome::AfterOrderSearch" type="Something\Awesome\Plugin\Order\OrderRepositoryPlugin" sortOrder="100" disabled="false"/>
</type>

Plugin class

<?php

namespace Something\Awesome\Plugin\Order;

use Magento\Sales\Api\Data\OrderSearchResultInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Model\Order;
use Something\Awesome\Model\MySpecialAttributeFactory;

class OrderRepositoryPlugin
{
    public function afterGetList(
        OriginalOrderRepository $orderRepository,
        OrderSearchResultInterface $orderSearchResult
    ) {
      $orders = $orderSearchResult->getItems();

      array_walk(
          $orders,
          function (Order $item) {
              $orderExtensionAttributes = $item->getExtensionAttributes();

              $orderExtensionAttributes->setMySpecialStringAttribute('Something special');
              // remember this setter depends on the code that you 
              // specify for your attribute inside extesion_attributes.xml
          }
      );

      return $orderSearchResult;

    }
}

extension_attributes.xml

Inside this file you can specify which fields are supposed to be added to your specific interface. Given that interface extends the \Magento\Framework\Api\ExtensibleDataInterface which it almost always is, except for your custom modules perhaps.

The basic structure is as follows:

<?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="my_special_attribute" type="string"/>
  </extension_attributes>
</config>

The code attribute is simply whatever you want your particular field to be named. The type attribute is a bit more tricky, you can't just specify anything in this field this has to be one of the three; scalar, non-scalar or an array.

Type: Scalar

Whenever we want to return a scalar type you can simply pas a scalar value given bool, int, float or a string. Simply specify the type as one of those and you get that value back in the API when you set it. Like so;

"extension_attributes": {
  "my_special_attribute": "Something special"
}

Keep in mind if you specify it as a string and return an int it'll still be a string. But if you specify an int and return a string it'll be 0. Just keep in mind to return the value you specify.

Type: array

An array is allowed when you give it a scalar value or non-scalar. What happens is that when you specify it for a scalar value like so:

<?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="my_special_array_attribute" type="string[]"/>
  </extension_attributes>
</config>

You'll get it the following way:

"extension_attributes": {
  "my_special_array_attribute": [
    "Something awesome",
    "Special",
    "String array"
  ]
}

Type non-scalar

The non-scalar values are the most versatile. This can be a class or an array of classes and thet can return an entire object. However this requires a bit more setup. To start with you can specify a non scalar value like so:

<?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="my_special_attribute" type="Something\Awesome\Api\Data\MySpecialAttributeInterface[]"/>
  </extension_attributes>
</config>

The contents of this interface are actually quite simple, you can specify the getters and the setters here. In this example it'll look something like this:

<?php

declare(strict_types=1);

namespace Something\Awesome\Api\Data;

interface MySpecialAttributeInterface
{
    public const LABEL               = 'label';
    public const VALUE               = 'value';
    public const REFERENCE_ID = 'reference_id';

    /**
     * @return string
     */
    public function getLabel(): string;

    /**
     * @param string $label
     *
     * @return $this
     */
    public function setLabel(string $label): self;

    /**
     * @return string
     */
    public function getValue(): string;

    /**
     * @param string $value
     *
     * @return $this
     */
    public function setValue(string $value): self;

    /**
     * @return int
     */
    public function getReferenceId(): int;

    /**
     * @param int $referenceId
     *
     * @return $this
     */
    public function setReferenceId(int $referenceId): self;
}

Because we need a model to instantiate this we build a model class as follows

<?php

declare(strict_types=1);

namespace Something\Awesome\Model;

class MySpecialAttribute extends DataObject implements MySpecialAttributeInterface
{
    public function getLabel(): string 
    {
        return $this->getData(self::LABEL);
    } 

    public function setLabel(string $label): self
    {
        return $this->setData(self::LABEL, $label);
    }

    public function getValue(): string
    {
        return $this->getData(self::VALUE);
    }

    public function setValue(string $value): self
    {
        return $this->setData(self::VALUE, $value);
    }

    public function getReferenceId(): int
    {
        return $this->getData(self::REFERENCE_ID);
    }

    public function setReferenceId(
        int $referenceId
    ): self {
        return $this->setData(self::REFERENCE_ID, $referenceId);
    }
}

This is simply an example, your class can be more complex if needed. Also I'm not extending the AbstractModel here because I'm not connecting this to any database tables.

Also a caveat is that you need to specify the docblocks for your interface. Magento will try to map the return value according to those and will raise an error when you miss them while returning the object in the API.

After this we can specify the following inside the plugin

<?php

namespace Something\Awesome\Plugin\Order;

use Magento\Sales\Model\OrderRepository as OriginalOrderRepository;
use Magento\Sales\Api\Data\OrderSearchResultInterface;
use Something\Awesome\Model\MySpecialAttributeFactory;

class OrderRepositoryPlugin
{
    public function __construct(
      private readonly MySpecialAttributeFactory $mySpecialAttributeFactory 
    ) {
    }

    public function afterGetList(
        OriginalOrderRepository $orderRepository,
        OrderSearchResultInterface $orderSearchResult
    ): OrderSearchResultInterface {
        if ($orderSearchResult->getTotalCount() === 0) {
            return $orderSearchResult;
        }

        $orders = $orderSearchResult->getItems();

        array_walk(
            $orders,
            function (Order $item) {
                $orderExtensionAttributes = $item->getExtensionAttributes();

                // some data to feed into the factory
                $mySpecialData = [
                    ['label' => 'something', 'value' => 'very special', 'reference_id' => 1234],
                    ['label' => 'something2', 'value' => 'very special2', 'reference_id' => 5678]
                ];

                $orderExtensionAttributes->setMySpecialAttribute(
                    array_map(
                        function ($specialItem) {
                            return $this->mySpecialAttributeFactory->create(['data' => $specialItem]);
                        },
                        $mySpecialData
                    )
                );
            }
        );

        return $orderSearchResult;
    }
}

This returns something like this in the API;

 ...
    "extension_attributes": {
      "my_special_attribute": [
        {
          "label": "something",
          "value": "very special",
          "reference_id": 1234,
        },
        {
          "label": "something2",
          "value": "very special2",
          "reference_id": 5678,
        }
      ]
    }
  }
...

I've set up a github module to serve as an example. See here.

Now you know how to return anything and everything in the Magento 2 API.

sources: https://developer.adobe.com/commerce/php/development/components/add-attributes/

0
Subscribe to my newsletter

Read articles from Wahid Nory directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Wahid Nory
Wahid Nory