Create custom Magento checkout layout processors to modify jsLayout

Still using plugins to modify properties of jsLayout? Stop right now, and learn a more elegant way to customize the Magento checkout with your own custom layout processor.

Mark Shust

Mark Shust

14 min read

What is a custom layout processor in Magento? To answer this question, we’ll need to take a step back and first learn how a page layout is rendered in Magento. Specifically, pages rendered with JavaScript, such as Magento’s one page checkout.

Overview of the JavaScript initialization process for Magento’s checkout

Magento’s checkout page is considered a “one page” checkout, because the initial page is loaded with a server-side route, and rendered with layout XML. We can confirm this by opening up Magento Checkout’s routes.xml file:

Magento_Checkout::etc/frontend/routes.xml
<?xml version="1.0"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="checkout" frontName="checkout">
            <module name="Magento_Checkout" />
        </route>
    </router>
</config>

And confirm the frontName="checkout", which will control all URLs starting with /checkout, including Magento’s checkout.

Since we know the /checkout route loads layout XML files with the naming convention of checkout_index_index.xml, the appropriate layout file within this Checkout module is applied to this route:

Magento_Checkout::view/frontend/layout/checkout_index_index.xml
<?xml version="1.0"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="checkout" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block class="Magento\Checkout\Block\Onepage" name="checkout.root" template="Magento_Checkout::onepage.phtml" cacheable="false">
                <arguments>
                    <argument name="jsLayout" xsi:type="array">
                        ...

You’ll notice this layout XML file references the main content container, adding a sole <block> to this container named checkout.root. This block loads the onepage.phtml template file, which then initializes the JavaScript process with the script tag:

Magento_Checkout::view/frontend/web/onepage.phtml
<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

/** @var $block \Magento\Checkout\Block\Onepage */
/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */
?>

<div id="checkout" data-bind="scope:'checkout'" class="checkout-container">
    ...
    <script type="text/x-magento-init">
        {
            "#checkout": {
                "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?>
            }
        }
    </script>
    ...

This magical little piece of code is what hands off the remainder of the checkout rendering process to JavaScript.

Where does jsLayout come from?

In this PHTML template aove, the $block variable is typehinted to the Onepage class:

/** @var $block \Magento\Checkout\Block\Onepage */

This tells us that $block->getJsLayout() references \Magento\Checkout\Block\Onepage::getJsLayout(). When we can inspect this function, we can see where this magic takes place:

Magento_Checkout::Block/Onepage.php
...
/**
 * @inheritdoc
 */
public function getJsLayout()
{
    foreach ($this->layoutProcessors as $processor) {
        $this->jsLayout = $processor->process($this->jsLayout);
    }

    return $this->serializer->serialize($this->jsLayout);
}
...

Here is where we finally see a reference to something called a “layout processor”. This code loops over all layout processors, which in turn continue modifying the jsLayout.

Layout processor: an example

There is a default layout processor located in the Magento Checkout module. If we look in it’s process() function, we’ll notice it carrying out some extra tasks relating to address attributes, shipping & billing fields:

Magento_Checkout::Block/Checkout/LayoutProcessor.php
...
/**
 * Process js Layout of block
 *
 * @param array $jsLayout
 * @return array
 */
public function process($jsLayout)
{
    $attributesToConvert = [
        'prefix' => [$this->options, 'getNamePrefixOptions'],
        'suffix' => [$this->options, 'getNameSuffixOptions'],
    ];

    $elements = $this->getAddressAttributes();
    $elements = $this->convertElementsToSelect($elements, $attributesToConvert);
    // The following code is a workaround for custom address attributes
    if (isset(
        $jsLayout['components']['checkout']['children']['steps']['children']['billing-step']['children']['payment']
        ['children']
    )) {
        $jsLayout['components']['checkout']['children']['steps']['children']['billing-step']['children']
        ['payment']['children'] = $this->processPaymentChildrenComponents(
            $jsLayout['components']['checkout']['children']['steps']['children']['billing-step']['children']
            ['payment']['children'],
            $elements
        );
    }
    
    ...
    
    return $jsLayout;
}
...

The $jsLayout variable is passed in as an argument, modified, and then returned as value to the function call of getJsLayout().

In much the same way this jsLayout was modified using a core built-in layout processor, we can create our own layout processor to modify just about any part of the jsLayout.

Why not modify layout components with XML?

If you are familiar with UI Components, you will be familiar with the role that layout XML plays to inject values & properties into JavaScript components.

As a quick summary, values defined within a UI Component’s config array with XML:

Foo_Bar::view/frontend/layout/checkout_index_index.xml
...
<item name="myCustomUiComponent" xsi:type="array">
    <item name="component" xsi:type="string">uiComponent</item>
    <item name="config" xsi:type="array">
        <!-- Setting a value for "myKey"... -->
        <item name="myKey" xsi:type="string">myValue</item>
    </item>
</item>
...

...can be accessed as a property of the related UI Component:

Foo_Bar::view/frontend/web/js/my-custom-ui-component.js
define([
    'uiComponent'
], function(
    Component
) {
    'use strict';

    return Component.extend({
        initialize: function() {
            ...
            // "myKey" is now accessible within the UI component:

            console.log(this.myKey); // Outputs "myValue"
        }
    });
});

The values within this layout XML can of course be overridden within custom modules, following Magento’s standard XML fallback merging process. This overriding process is one of the great strengths of Magento.

In almost all scenarios, you will want to define properties and value overrides just like this with XML.

In specific circumstances, this may not be possible though, as a large amount of XML will come with the increased risk of introducing either human error or reduced code maintainability. A great example of this scenario is when dealing with fields that are dynamically created during the checkout, such as those related to payment methods. Since payment methods can be toggled on or off from Magento’s admin or created with third-party modules, their dynamic nature creates a liability to updates written in XML.

It's also possible you may have unique client requirements in which you'll need to be able to easily duplicate or move blocks of components to other areas of the jsLayout, and this would be extremely time-consuming process to write or handle with XML.

For these scenarios, you will almost always wish to create your own custom layout processors to process these dynamic situations.

Enjoying this article? Then you may be interested in my "Customize the Magento 2 Checkout" course which was recently released.

We'll go over layout processors step-by-step in much greater detail, along with modifying all other areas of Magento's Onepage checkout:

Implementing your own custom layout processor

If you’ve made it this far, give yourself a pat on the back. The checkout is definitely the most complex areas of Magento, and is very difficult to understand without an extensive breakdown of each specific related topic.

Creating your own custom layout processor is actually extremely simple. Your first step is to make Magento aware of your layout processor by adding it as a value to the $layoutProcessors array of the OnePage class.

This can easily be done by using argument substitution with a di.xml file:

Foo_Bar::etc/frontend/di.xml
<?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\Checkout\Block\Onepage">
        <arguments>
            <argument name="layoutProcessors" xsi:type="array">
                <item name="foo_bar_updateaddresssortorder" xsi:type="object">Foo\Bar\Block\Checkout\LayoutProcessor\UpdateAddressSortOrder</item>
            </argument>
        </arguments>
    </type>
</config>

Since you depend on the Magento_Checkout module now, you’ll also want to add that module as a dependency to your module:

Foo_Bar::etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Foo_bar">
        <sequence>
            <module name="Magento_Checkout"/>
        </sequence>
    </module>
</config>

Then, you can create a new class which implements the LayoutProcessorInterface, to apply any update you wish with PHP.

Within your custom layout processor, you have access to the full jsLayout object which will be rendered in the checkout.

In this layout processor, we’re just updating the sort order of the city, region_id and postcode address fields. This code will override any values defined with XML, and update the related address fields sortOrder value.

Foo_Bar::Block/Checkout/LayoutProcessor/UpdateAddressSortOrder
<?php declare(strict_types=1);

namespace Foo\Bar\Block\Checkout\LayoutProcessor;

use Magento\Checkout\Block\Checkout\LayoutProcessorInterface;

class UpdateAddressSortOrder implements LayoutProcessorInterface
{
    public function process($jsLayout): array
    {
        foreach ($jsLayout['components']['checkout']['children']
            ['steps']['children']
            ['billing-step']['children']
            ['payment']['children']
            ['payments-list']['children'] as &$paymentMethod) {
            $fields = &$paymentMethod['children']['form-fields']['children'];
            if ($fields === null) {
                continue;
            }
            $fields['city']['sortOrder'] = '72';
            $fields['region_id']['sortOrder'] = '74';
            $fields['postcode']['sortOrder'] = '76';
        }

        return $jsLayout;
    }
}

The same functionality would be extremely hard to write with XML, because we are iterating through every payment method and applying updates to each of the children form fields. Creating this same functionality with XML would lead to lots of redundant code, making it hard to diagnose & debug in future updates.

Conclusion

I hope this blog post help made sense of exactly how layout processors are created and added to the JavaScript rendering process of Magento's checkout, and how you can create your own layout processor to accomplish dynamic tasks in a more streamlined manner.

Want new Magento blog posts?

Blog feed reader? Grab the RSS Feed URL
M.academy supports Ukraine 🇺🇦