Adding Custom Attributes to Magento 2 Quotes and Orders

Magento 2 provides some default attributes that are added to the quote and order item data. Here is how to go about adding custom Attributes to Magento 2 quotes and orders.

If you’ve read Adding Custom Attributes to Quotes and Orders in Magento 1, then the below steps aren’t too dissimilar from adding custom attributes in the original versions of Magento.

Assuming that you’ve already defined a custom module with a registration.php and a module.xml file, then you are ready to add the code from the steps below.

To start with, define the custom attribute in a catalog_attributes.xml within a quote_item group.

In this example, a dropdown_attribute attribute (of type dropdown) will be added to Magento.


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Catalog:etc/catalog_attributes.xsd">
    <group name="quote_item">
        <attribute name="dropdown_attribute"/>
    </group>
</config>

Add the InstallData.php class to add the custom attribute programmatically. In this example, the attribute created will be added to the Default attribute set, within the General group.

Take note that the attribute is also added as a column to the quote_item and sales_order_item tables via $quoteSetup and $salesSetup respectively.


<?php
namespace [Vendor]\[Module]\Setup;

use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Sales\Setup\SalesSetupFactory;
use Magento\Quote\Setup\QuoteSetupFactory;

/**
 * @codeCoverageIgnore
 */
class InstallData implements InstallDataInterface
{
    /**
     * EAV setup factory
     *
     * @var EavSetupFactory
     */
    private $eavSetupFactory;

    /**
     * @var QuoteSetupFactory
     */
    private $quoteSetupFactory;

    /**
     * @var SalesSetup
     */
    private $salesSetupFactory;

    /**
     * InstallData constructor.
     * @param EavSetupFactory $eavSetupFactory
     * @param QuoteSetupFactory $quoteSetupFactory
     */
    public function __construct(
        EavSetupFactory $eavSetupFactory,
        QuoteSetupFactory $quoteSetupFactory,
        SalesSetupFactory $salesSetupFactory
    )
    {
        $this->eavSetupFactory = $eavSetupFactory;
        $this->quoteSetupFactory = $quoteSetupFactory;
        $this->salesSetupFactory = $salesSetupFactory;
    }

    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        /** @var EavSetup $eavSetup */
        $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);

        /** @var QuoteSetup $quoteSetup */
        $quoteSetup = $this->quoteSetupFactory->create(['setup' => $setup]);

        /** @var SalesSetup $salesSetup */
        $salesSetup = $this->salesSetupFactory->create(['setup' => $setup]);

        /**
         * Add attributes to the eav/attribute
         */
        $eavSetup->addAttribute(
            \Magento\Catalog\Model\Product::ENTITY,
            'dropdown_attribute',
            [
                'type'                    => 'int',
                'label'                   => 'Dropdown Attribute',
                'input'                   => 'select',
                'global'                  => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL,
                'visible'                 => true,
                'required'                => false,
                'user_defined'            => true,
                'default'                 => '',
                'searchable'              => false,
                'filterable'              => false,
                'comparable'              => false,
                'visible_on_front'        => false,
                'used_in_product_listing' => false,
                'unique'                  => false,
                'option'                  => [
                    'values' => [
                        'Option 1',
                        'Option 2',
                        'Option 3'
                    ],
                ]
            ]
        );

        $attributeSetId = $eavSetup->getDefaultAttributeSetId('catalog_product');
        $eavSetup->addAttributeToSet(
            'catalog_product',
            $attributeSetId,
            'General',
            'dropdown_attribute'
        );

        $attributeOptions = [
            'type'     => Table::TYPE_TEXT,
            'visible'  => true,
            'required' => false
        ];
        $quoteSetup->addAttribute('quote_item', 'dropdown_attribute', $attributeOptions);
        $salesSetup->addAttribute('order_item', 'dropdown_attribute', $attributeOptions);
    }
}

The next step is to ensure that the product dropdown attribute is saved in the quote when the sales_quote_item_set_product event is fired.

Add an observer to this event via the events.xml file.


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="sales_quote_item_set_product">
        <observer name="set_item_dropdown_attribute" instance="[Vendor]\[Module]\Observer\SetItemDropdownAttribute" />
    </event>
</config>

Now add the SetItemCustomAttribute.php observer. This will simply set the product’s dropdown attribute option, which can be retrieved using $product->getAttributeText('dropdown_attribute'), to the quote item using setDropdownAttribute().


<?php
namespace [Vendor]\[Module]\Observer;

use Magento\Framework\Event\ObserverInterface;

class SetItemDropdownAttribute implements ObserverInterface
{
    /**
     * @param \Magento\Framework\Event\Observer $observer
     * @return void
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        $quoteItem = $observer->getQuoteItem();
        $product = $observer->getProduct();
        $quoteItem->setDropdownAttribute($product->getAttributeText('dropdown_attribute'));
    }
}

The last step should be (see explanation below…) to add a fieldset.xml file that defines the attribute to be copied over from the quote to order object when an order is placed. This is similar to what happens in Magento 1 using the module’s config.xml file.


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Object/etc/fieldset.xsd">
    <scope id="global">
        <fieldset id="quote_convert_item">
            <field name="dropdown_attribute">
                <aspect name="to_order_item" />
            </field>
        </fieldset>
    </scope>
</config>

If you now enable the module, clear the cache and run the database upgrade via the commands provided, you should notice that the dropdown_attribute attribute has been added to Magento’s eav_attribute table in the database, and also as a column to the quote_item and sales_order_item tables.

Now assign an existing product a value from the dropdown_attribute attribute within the admin, and when adding the product to your basket, you should notice that the dropdown_attribute column in the quote_item table is now populated for the item entry!

Finally, place an order which will convert the quote item data to an order item entry in the sales_order_item table. Whilst the default column data from quote_item is copied across, the dropdown_attribute column is not.

It is not clear whether this is intentional from Magento, or whether it is more likely a bug, but for some reason the fieldset.xml configuration in custom modules doesn’t take into account the conversion of the quote item to order item.

Therefore to fix this problem, create a plugin that will function around the convert() method of the Magento\Quote\Model\Quote\Item\ToOrderItem class. This is the class responsible for converting the quote data to order data.

Start by defining the plugin within your module’s di.xml file.


<?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\Quote\Model\Quote\Item\ToOrderItem">
        <plugin name="dropdown_attribute_quote_to_order_item" type="[Vendor]\[Module]\Plugin\DropdownAttributeQuoteToOrderItem"/>
    </type>
</config>

Then add the aroundConvert() plugin.


<?php
namespace [Vendor]\[Module]\Plugin;

class DropdownAttributeQuoteToOrderItem
{
    public function aroundConvert(
        \Magento\Quote\Model\Quote\Item\ToOrderItem $subject,
        \Closure $proceed,
        \Magento\Quote\Model\Quote\Item\AbstractItem $item,
        $additional = []
    ) {
        /** @var $orderItem \Magento\Sales\Model\Order\Item */
        $orderItem = $proceed($item, $additional);
        $orderItem->setDropdownAttribute($item->getDropdownAttribute());
        return $orderItem;
    }
}

Now when you place an order, the quote item’s dropdown_attribute value will copy across to the sales_order_item table.

Hopefully the plugin step will not be needed in the future when Magento fix the conversion issue. However for now, the plugin solution is sufficient.

Note: This article is based on Magento Open Source version 2.2.