Adding a Field to Checkout

The four parts of a custom checkout field: layout processor, extension attribute, saving to the quote, copying to the order.

Syntaxpublic function process($jsLayout) // Magento\Checkout\Block\Checkout\LayoutProcessorInterface

Adding one field to checkout is four jobs, not one: render it, carry it to the server, store it on the quote, and copy it to the order. The quote is the cart record. The order is created from the quote when the shopper places it, and nothing on the quote reaches the order by itself.

Render it

A layout processor is a class implementing Magento\Checkout\Block\Checkout\LayoutProcessorInterface. Its process($jsLayout) method receives the merged component tree as a PHP array and returns it changed. Register it as an item in the layoutProcessors array argument of Magento\Checkout\Block\Onepage in etc/frontend/di.xml, then run bin/magento cache:flush.

Carry it

An extension attribute is a field added to an API data interface from outside the module that owns it, declared in etc/extension_attributes.xml. Declaring one on Magento\Quote\Api\Data\AddressInterface lets the value ride along with the shipping address. Its getter is generated code, so run bin/magento setup:di:compile or the method does not exist. Moving the value from the form into that request is a JavaScript job, done with a mixin on the shipping save processor.

Store it, then copy it

A before plugin on Magento\Checkout\Api\ShippingInformationManagementInterface::saveAddressInformation reads the attribute and writes it to the quote. An observer of sales_model_service_quote_submit_before copies it from quote to order. Both need real columns, declared in db_schema.xml and applied with bin/magento setup:upgrade.

Example

Example · php
<?php
// app/code/Vendor/DeliveryNote/Block/Checkout/LayoutProcessor.php
declare(strict_types=1);

namespace Vendor\DeliveryNote\Block\Checkout;

use Magento\Checkout\Block\Checkout\LayoutProcessorInterface;

class LayoutProcessor implements LayoutProcessorInterface
{
    public function process($jsLayout)
    {
        // Reference into the shipping address fieldset, so the assignment below
        // lands back in $jsLayout.
        $fields = &$jsLayout['components']['checkout']['children']['steps']['children']
            ['shipping-step']['children']['shippingAddress']['children']
            ['shipping-address-fieldset']['children'];

        $fields['delivery_note'] = [
            'component' => 'Magento_Ui/js/form/element/textarea',
            'provider' => 'checkoutProvider',
            'dataScope' => 'shippingAddress.extension_attributes.delivery_note',
            'label' => __('Delivery note'),
            'sortOrder' => 250,
            'validation' => [
                'max_text_length' => 255,
            ],
            'config' => [
                'template' => 'ui/form/field',
                'elementTmpl' => 'ui/form/element/textarea',
                'rows' => 3,
                'id' => 'delivery-note',
            ],
        ];

        return $jsLayout;
    }
}

When to use it

  • Collect a delivery note or a purchase order reference during checkout.
  • Add a required checkbox for terms specific to one store view.
  • Show a collected value on the admin order view and in the order confirmation email.
  • Pass a shopper-entered reference through to an ERP integration reading the order.

More examples

Declaring the extension attribute

This adds getDeliveryNote() and setDeliveryNote() to the extension attributes object of every quote address, including the one that arrives inside the shipping-information web API call. The attribute code is snake_case because that is what the JSON payload carries; the generated methods are camelCase. The methods live in generated/code, so run bin/magento setup:di:compile after adding or renaming an attribute. Calling the getter before compiling raises an undefined method error that looks like a typo in your plugin.

Example · xml
<!-- app/code/Vendor/DeliveryNote/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\Quote\Api\Data\AddressInterface">
        <attribute code="delivery_note" type="string"/>
    </extension_attributes>
</config>

Saving the value onto the quote

A before plugin is the right shape here: it needs the arguments, not the return value, and it must run before the shipping address is persisted. It returns the full argument list unchanged, which is what a before plugin must do when it returns anything at all. Register it on the interface, not the concrete class, in etc/di.xml, and run bin/magento setup:di:compile in production mode. Note getExtensionAttributes() can return null on a hand-built request, so the guard is not optional.

Example · php
<?php
// app/code/Vendor/DeliveryNote/Plugin/SaveDeliveryNote.php
declare(strict_types=1);

namespace Vendor\DeliveryNote\Plugin;

use Magento\Checkout\Api\Data\ShippingInformationInterface;
use Magento\Checkout\Api\ShippingInformationManagementInterface;
use Magento\Quote\Api\CartRepositoryInterface;

class SaveDeliveryNote
{
    public function __construct(
        private readonly CartRepositoryInterface $cartRepository
    ) {
    }

    public function beforeSaveAddressInformation(
        ShippingInformationManagementInterface $subject,
        $cartId,
        ShippingInformationInterface $addressInformation
    ): array {
        $extension = $addressInformation->getShippingAddress()->getExtensionAttributes();
        $note = $extension === null ? null : $extension->getDeliveryNote();

        if ($note !== null) {
            $quote = $this->cartRepository->getActive($cartId);
            $quote->setData('delivery_note', $note);
            $this->cartRepository->save($quote);
        }

        return [$cartId, $addressInformation];
    }
}

Copying it to the order

Magento\Quote\Model\QuoteManagement dispatches sales_model_service_quote_submit_before with both the quote and the unsaved order, which is the last moment to add data that must persist with the order. Register the observer in etc/events.xml under that event name and run bin/magento cache:flush. Setting the value after the order is saved, for example on checkout_submit_all_after, writes to an object nobody saves again.

Example · php
<?php
// app/code/Vendor/DeliveryNote/Observer/CopyNoteToOrder.php
declare(strict_types=1);

namespace Vendor\DeliveryNote\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;

class CopyNoteToOrder implements ObserverInterface
{
    public function execute(Observer $observer): void
    {
        $quote = $observer->getEvent()->getData('quote');
        $order = $observer->getEvent()->getData('order');

        $note = $quote->getData('delivery_note');

        if ($note !== null && $note !== '') {
            $order->setData('delivery_note', $note);
        }
    }
}

Discussion

  • Be the first to comment on this lesson.