Dynamically Adding Extension Attributes to Orders in Magento 2: A Flexible Approach

Extension attributes are a powerful mechanism in Magento 2 for extending core functionality without modifying the core codebase. They provide a clean and upgrade-safe way to add custom data to entities like products, customers, and orders.

In this post, we'll explore a flexible approach to dynamically add extension attributes to orders using a combination of plugins and di.xml configuration. This method allows you to easily add or modify extension attributes without altering any core files, making it a more maintainable and scalable solution.

Why Extension Attributes?

Before diving into the implementation, let's recap why extension attributes are the preferred way to extend core entities:

  • Maintainability: Avoids direct modification of core files, ensuring easier upgrades and reducing conflicts.
  • Upgrade Compatibility: Extension attributes are designed to be forward-compatible with future Magento versions.
  • Modularity: Encourages a modular architecture by keeping customizations separate from core code.
  • API Compatibility: Extension attributes are exposed through the Magento API, making them accessible to external systems.

Dynamically Adding Extension Attributes to Orders in Magento 2: A Flexible Approach

Extension attributes are a powerful mechanism in Magento 2 for extending core functionality without modifying the core codebase. They provide a clean and upgrade-safe way to add custom data to entities like products, customers, and orders.

While the standard approach to populate extension attributes is via plugins, it often involves hardcoding the logic for each attribute within the plugin's afterGet or afterGetList methods. This can lead to code bloat and maintenance challenges as the number of attributes grows.

In this post, we'll explore a more flexible approach to dynamically add extension attributes to orders using a combination of plugins and di.xml configuration. This method allows you to easily add or modify extension attributes without altering the plugin's code, making it a more maintainable and scalable solution.

Standard Approach to Populate Extension Attributes

Before diving into the dynamic approach, let's briefly review the standard way of populating extension attributes using plugins, as demonstrated in this example from Adobe: Adding attributes.

This approach typically involves:

  1. Retrieving the entity's extension attributes using $entity->getExtensionAttributes().
  2. Retrieving or calculating the custom data for the attribute.
  3. Setting the attribute value on the extension attributes object.
  4. Setting the modified extension attributes back on the entity using $entity->setExtensionAttributes().

While effective, this method requires code changes within the plugin every time you add or modify an attribute.

Dynamic Extension Attributes with Plugins and di.xml

Our approach leverages a custom plugin that intercepts order retrieval methods and dynamically adds extension attributes based on configuration in di.xml. This allows you to define the mapping between extension attribute codes and the corresponding data keys or hydrator classes in a centralized location.

Key Components:

  1. extension_attributes.xml: This file defines the extension attributes themselves, specifying their code and data type.
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="custom_attribute_1" type="string"/>
        <attribute code="custom_attribute_2" type="int"/>
        <attribute code="complex_attribute" type="string"/> 
    extension_attributes>
config>
  1. di.xml: This file configures the plugin and defines the mapping between extension attribute codes and data sources.
XML
"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="test_order_attributes_plugin" type="TestOrderAttributes\Plugin\Sales\Model\Order\OrderAttributesPlugin" sortOrder="10" />
    type>
    <type name="TestOrderAttributes\Plugin\Sales\Model\Order\OrderAttributesPlugin">
        <arguments>
            <argument name="attributes" xsi:type="array">
                <item name="custom_attribute_1" xsi:type="string">customer_emailitem> 
                <item name="custom_attribute_2" xsi:type="string">total_qty_ordereditem> 
                <item name="complex_attribute" xsi:type="object">TestOrderAttributes\Model\Order\Attribute\ComplexAttributeitem> 
            argument>
        arguments>
    type>
config>
  1. Plugin Class: The plugin intercepts order retrieval methods (afterGet, afterGetList) and populates the extension attributes based on the di.xml configuration.
PHP


namespace TestOrderAttributes\Plugin\Sales\Model\Order;

use Magento\Sales\Api\Data\OrderExtensionInterface;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\Data\OrderSearchResultInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use TestOrderAttributes\Api\OrderAttributeHydratorInterface;

class OrderAttributesPlugin
{
    private $attributes;

    public function __construct(
        array $attributes =
    ) {
        $this->attributes = $attributes;
    }

    public function afterGet(OrderRepositoryInterface $subject, OrderInterface $order): OrderInterface
    {
        $this->setAttributes($order, $order->getExtensionAttributes());
        return $order;
    }

    public function afterGetList(
        OrderRepositoryInterface $subject,
        OrderSearchResultInterface $orders
    ): OrderSearchResultInterface {
        foreach ($orders->getItems() as $order) {
            $this->setAttributes($order, $order->getExtensionAttributes());
        }
        return $orders;
    }

    private function setAttributes(OrderInterface $order, ?OrderExtensionInterface $orderExtension = null): void
    {
        $entity = $orderExtension ?? $order;
        foreach ($this->attributes as $attributeCode => $attributeValue) {
            if ($attributeValue instanceof OrderAttributeHydratorInterface) {
                $value = $attributeValue->hydrate($order);
            } else {
                $value = $order->getData($attributeValue);
            }
            $entity->setData($attributeCode, $value);
        }
    }
}
  1. Hydrator Classes (Optional): For complex attribute logic, you can create dedicated hydrator classes that implement an interface (e.g., OrderAttributeHydratorInterface).
PHP


namespace TestOrderAttributes\Model\Order\Attribute;

use Magento\Sales\Api\Data\OrderInterface;
use TestOrderAttributes\Api\OrderAttributeHydratorInterface;

class ComplexAttribute implements OrderAttributeHydratorInterface
{
    public function hydrate(OrderInterface $order)
    {
        // Example: Combine customer group ID and order status
        return $order->getCustomerGroupId() . '_' . $order->getStatus();
    }
}

Benefits of this Approach

  • Flexibility: Easily add or modify extension attributes by simply updating the di.xml configuration.
  • Maintainability: Keeps your code clean and organized, reducing the risk of conflicts and making upgrades smoother.
  • Scalability: Easily handle a growing number of extension attributes without code bloat.
  • Centralized Configuration: All attribute mappings are defined in one place (di.xml).

Conclusion

This dynamic approach to adding extension attributes to orders provides a flexible and maintainable solution for extending core functionality in Magento 2. By leveraging plugins and di.xml configuration, you can easily manage and customize order data without modifying core files, ensuring a more robust and upgrade-safe implementation.

This approach is particularly beneficial when dealing with a large number of extension attributes or frequent changes, as it minimizes code modifications and promotes a more declarative approach to customization.