Magento 2 Frontend: layout changes

we’re going to provide an engineering critique of Magento 2’s XML based layout rendering language. This language is similar to the XML based language in Magento 1, but has some differences that might trip up an experienced Magento 1 developer. 

Magento 2’s Domain Specific Language for Rendering HTML

With Magento 2, the core team has added a number of features to their XML based layout language. They’ve also changed some of the language’s semantics. For example, in Magento 1, each node under the root node of a layout update XML file was always a layout handle. For example, in the Magento 1 Layout Update XML Node below, the handle is catalog_category_view.

Magento 1: catalog.xml

<catalog_category_view>
    <block ...>
        <!-- ... -->
    </block>
</catalog_category_view

 

Without getting too deeply into it, handles control which layout nodes are applied to a page during which request. Magento 2 still has handles, but they no longer appear in layout files. Instead, the handle is the file name. For example, the main catalog_category_view layout handle XML file is at

./vendor/magento/module-catalog/view/frontend/layout/catalog_category_view.xml

Other modules that listen for the catalog_category_view handle include

./vendor/magento/module-checkout/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-directory/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-google-optimizer/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-msrp/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-paypal/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-swatches/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-wishlist/view/frontend/layout/catalog_category_view.xml

In all the examples, the file name is the handle name. While the mechanism has changed, handles still serve the same purpose in Magento 2. On every Magento 2 HTML page render, certain handles “fire”, similar to events. The handles that fire control which layout files are loaded (in Magento 1 they controlled which XML nodes were read from all the files), and then Magento processes the combined XML tree to know which blocks it should add to a page.

 

Put another way, in Magento 1 you would

  1. Configure your module to load a layout update XML file (namespace_module.xmlcatalog.xml etc.)
  2. In the layout update XML file you’d add a node for your handle (catalog_category_view)
  3. Under the catalog_category_view node you’d add your blocks

 

In Magento 2, you

  1. Add a layout handle XML file (catalog_category_view.xml)
  2. Under the root node of your layout handle XML file, add your blocks

 

We covered this change briefly in our Introduction to Magento 2 — No More MVC article. However, we glossed over a number of substantial changes to Magento’s layout language that we’ll need to touch briefly on before we continue.

 

New Nodes 

In our Introduction to Magento 2 — No More MVC article, we created the following node in the layout handle XML file.

app/code/Ecommage/HelloWorldMVVM/view/frontend/layout/hello_mvvm_hello_world.xml

<referenceBlock name="content">
    <block
        template="content.phtml"
        class="Ecommage\HelloWorldMVVM\Block\Main"
        name="Ecommage_helloworld_mvvm"/>
</referenceBlock>

 

As a reminder, this XML is roughly equivalent to the following pseudo code. 

$our_view_block = $layout->createNewBlockWithClass('Ecommage\HelloWorldMVVM\Block\Main')
$our_view_block->setName('Ecommage_helloworld_mvvm');
$out_view_block->setTemplate('content.phtml');
$layout->addBlockToContentContainer($our_view_block);

The <block/> tag from Magento 1 remains relatively unchanged. It means Create a block object. The main difference is the type attribute has been replaced with a class attribute. Since Magento did away with class aliases (core/templatenamespace_module/my_block, etc.) it made sense to do away with the typeattribute, and more accurately label it as class.

The first small change above is the referenceBlock node. Magento 1 had the concept of a block reference. However, the node was named <reference/>. In Magento 1, the above might look like

app/code/Ecommage/HelloWorldMVVM/view/frontend/layout/hello_mvvm_hello_world.xml

<reference name="content">
    <block
        template="content.phtml"
        class="Ecommage\HelloWorldMVVM\Block\Main"
        name="Ecommage_helloworld_mvvm"/>
</block>

 

The referenceBlock node makes things more explicit, and is another welcome change. This might seem superficial, until you realize that Magento 2’s layout language controls more than blocks. The layout language also controls something called containers, and has a corresponding referenceContainer block. You can see an example of the referenceContainer block here

vendor/magento/module-checkout/view/frontend/layout/checkout_shipping_price_renderer.xml

<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd">
    <referenceContainer name="root">
        <block class="Magento\Checkout\Block\Shipping\Price" name="checkout.shipping.price" as="shipping.price" template="shipping/price.phtml"/>
    </referenceContainer>
</layout>

In Magento 2, a container is a special sort of block that only contains other blocks. Containers are conceptually similar to the text/list blocks in Magento 1, although their implementation is very different.

The concept of containers is a good one, but it’s here that the implementation starts to get a little wobbly. Magento’s layout language is a little loosey–goosey with the difference between a container and a block. For example, the above XML?

vendor/magento/module-checkout/view/frontend/layout/checkout_shipping_price_renderer.xml

<referenceContainer name="root">

 

This could also be written as

vendor/magento/module-checkout/view/frontend/layout/checkout_shipping_price_renderer.xml

<referenceBlock name="root">

That is — even though root is a container, referenceBlock will still return a reference to it, and allow you to add blocks to it. For a change meant to make things more explicit and clear, it’s a little strange that the layout language would let something like that happen.

 

White Lies

Remember this XML from the introduction article?

app/code/Ecommage/HelloWorldMVVM/view/frontend/layout/hello_mvvm_hello_world.xml

<referenceBlock name="content">
    <block
        template="content.phtml"
        class="Ecommage\HelloWorldMVVM\Block\Main"
        name="Ecommage_helloworld_mvvm"/>
</referenceBlock>

Well, it turns out that the content block is actually a container. The above should have been written as

We used referenceBlock in our introduction tutorial because we weren’t ready to discuss containers and other changes to the layout system. While this was useful for a transitional tutorial, generally speaking this is the sort of looseness that can make a domain specific language seem extra confusing.

 

Without getting too deeply into the details, you can tell if a “block” is a container or a regular block by how the original programmer created it. If you see a <block/> tag

<block name="foo" />

then the named block (“foo” above) is a regular block. If you see a <container/> tag

<container name="foo"/>

 then the named entity is a container. If you’re curious, Magento’s core code adds the content container in the following file

vendor/magento/module-theme/view/frontend/layout/default.xml

<container name="content" label="Main Content Area"/>

Also, notice the default.xml file name? That’s equivalent to Magento 1’s <default/> handle node.

 

Context Sensitive Nodes

In Magento 1, the layout language was a system designed to render arbitrary HTML via a nested collection of block objects. The layout system itself didn’t care which part of an HTML document it was rendering. It just rendered blocks. Specific blocks, like page/html_head, could introduce that context, but it happened at the block level. The layout system itself was unaware that it was rendering the <head/> portion of a document.

In Magento 2, the core team attempted to change this, and add that context in at the language level. They added two new top level tags named <body> and <head> to the vocabulary of the language. While it was an interesting experiment, the implementation feels half done, and further complicates an already complicated layout system. Consider the following

vendor/magento/module-backend/view/adminhtml/layout/default.xml

 <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <head>
        <title>Magento Admin</title>
        <meta name="viewport" content="width=1024, initial-scale=1"/>
        <link src="requirejs/require.js"/>
        <css src="extjs/resources/css/ext-all.css"/>
        <css src="extjs/resources/css/ytheme-magento.css"/>
    </head>
    <body>
        <attribute name="id" value="html-body"/>
        <block name="require.js" class="Magento\Backend\Block\Page\RequireJs" template="Magento_Backend::page/js/require_js.phtml"/>
        <referenceContainer name="global.notices">
            <block class="Magento\Backend\Block\Page\Notices" name="global_notices" as="global_notices" template="page/notices.phtml"/>
        </referenceContainer>
        <!-- ... -->
    </body>
</page>

 

Here you can see an example of a core module layout handle XML file that uses the new head and body sections. The first bit of confusion this introduces is top level tags under the root tag now mean different things. In some files, these top level tags will be context tags like <head/> and <body/> above. In other files, the top level tags will be actual commands/directives (referenceBlockcontainer, etc) for the layout engine

vendor/magento/module-bundle/view/base/layout/catalog_product_prices.xml

<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd">
    <referenceBlock name="render.product.prices">
    <!-- ... -->
</layout>

 

If you enjoy implementing domain specific languages, this may seem like a minor thing. However, the intent of a domain specific language is to simplify and constrain the options for developers and programmers unfamiliar with the entire system. The lack of consistency here will make these files harder for designers and front end developers to understand.

The next bit of confusion is in what that context change means. Nodes placed inside the <body/> tag

vendor/magento/module-backend/view/adminhtml/layout/default.xml

<body>
    <attribute name="id" value="html-body"/>
    <block name="require.js" class="Magento\Backend\Block\Page\RequireJs" template="Magento_Backend::page/js/require_js.phtml"/>
    <referenceContainer name="global.notices">
        <block class="Magento\Backend\Block\Page\Notices" name="global_notices" as="global_notices" template="page/notices.phtml"/>
     </referenceContainer>
     <!-- ... -->
</body>

behave very similar to plain old layout XML nodes. You’re still getting references to existing blocks and containers, and adding new blocks to them for rendering. The only difference is the <attribute/> tag you see above. With this you can change the ID element of the underlying <body/> tag. 

When you shift into <head/> context, you’re in a difference world.

vendor/magento/module-backend/view/adminhtml/layout/default.xml

<head>
    <title>Magento Admin</title>
    <meta name="viewport" content="width=1024, initial-scale=1"/>
    <link src="requirejs/require.js"/>
    <css src="extjs/resources/css/ext-all.css"/>
    <css src="extjs/resources/css/ytheme-magento.css"/>
</head>

 

Here, you’ve completely lost the ability to modify the layout with commands like referenceBlock, etc. Instead, you have a narrow set of tags (<attribute/><css/><link/><meta/><remove/><script/>,<title/>) for doing things specifically in the <head/> of a document. 

The other bit of cognitive dissonance a Magento 1 developer will feel here is the <head/> section of the HTML page is no longer rendered like a normal block. If you take a look at the root phtml template you can see a Magento HTML page is no longer a series of nested blocks.

vendor/magento/module-theme/view/base/templates/root.phtml

<!doctype html>
<html <?php /* @escapeNotVerified */ echo $htmlAttributes ?>>
    <head <?php /* @escapeNotVerified */ echo $headAttributes ?>>
        <?php /* @escapeNotVerified */ echo $requireJs ?>            <?php /* @escapeNotVerified */ echo $headContent ?>            <?php /* @escapeNotVerified */ echo $headAdditional ?>        </head>
    <body data-container="body" data-mage-init='{"loaderAjax": {}, "loader": { "icon": "<?php /* @escapeNotVerified */ echo $loaderIcon; ?>"}}' <?php /* @escapeNotVerified */ echo $bodyAttributes ?>>
        <?php /* @escapeNotVerified */ echo $layoutContent ?>        </body>
</html>

In Magento 2, an HTML page is a phtml template populated by simple variables. These simple variables are populated by different means in the render method of the Magento\Framework\View\Result\Page object. Magento creates the <body/> tag of the page by echoing out the $layoutContent variable. Magento gets the string for $layoutContent by doing the traditional kickoff of rendering a series of nested blocks.

vendor/magento/framework/View/Result/Page.php

$output = $this->getLayout()->getOutput();
$this->assign('layoutContent', $output);
//...
<?php /* @escapeNotVerified */ echo $layoutContent ?>

Magento renders the <head/> section of an HTML page by echoing several variables.

 vendor/magento/framework/View/Result/Page.php

<head <?php /* @escapeNotVerified */ echo $headAttributes ?>>
    <?php /* @escapeNotVerified */ echo $requireJs ?>        <?php /* @escapeNotVerified */ echo $headContent ?>        <?php /* @escapeNotVerified */ echo $headAdditional ?>    </head>

How Magento populates of contents of these variable is beyond the scope of this article. The main change you’ll want to be aware of is that <head/> is no longer simply controlled by standard layout blocks.

 

Summary of Magento 2 Layout Changes

Magento 1’s layout system, while cryptic, was ultimately understandable by a single developer. The system wasn’t well documented, but once explained developers could understand and reason about it from top to bottom. It had a complex looking surface, but a simple elegant implementation.

As you can see from the above critique, Magento 2 has taken an already cryptic system, and added layers of complexity on top of it. These top levels of complexity are equally complex under the hood. Without getting into the details of it, Magento takes the layout handle XML files for a single request, merges them into a a single document, processes that document to transform it into a Magento 1 style page layout document, and then processes that document is way that similar, but not identical, to Magento 1.

Unlike Magento 1’s layout system, which an average developer could ultimately translate in their head into PHP code and reason about, the new rendering is too complex for most human beings to keep in their head at once. The new system is less understandable to the average developer. Perhaps this was necessary to implement the RequireJS and Less CSS systems the core team wanted to, but from the outside looking in it seems like a classic case of what people complain about when they complain about architect driven development.

Comment

There is no comment on this post. Be the first one.

Leave a comment