Magento2 Category Field Custom Layout Update – How to Add Handle and Declaration

categorylayoutlayout-updatemagento2

In Magento 2, has anyone tried to add an update handle in "Custom Layout Update" on the category admin page? I've set the following on a category:

<update handle="my_handle_lorem_ipsum_lorem" />

And I created the following file in my current theme:

[theme dir]/Magento_Catalog/layout/my_handle_lorem_ipsum_lorem.xml

However, the update handle doesn't appear to be applied.

I know the XML directives in my_handle_lorem_ipsum_lorem.xml are correct because they work correctly when placed in [theme dir]/Magento_Catalog/layout/catalog_category_view_id_123.xml.

By debugging I've found that the "Custom Layout Update" attribute is handled here:

// \Magento\Catalog\Controller\Category\View::execute() [excerpt]

$layoutUpdates = $settings->getLayoutUpdates();
if ($layoutUpdates && is_array($layoutUpdates)) {
    foreach ($layoutUpdates as $layoutUpdate) {
        $page->addUpdate($layoutUpdate);
    }
}

Tracing the source of $layoutUpdates, I found that it ultimately gets set in _extractSettings() of \Magento\Catalog\Model\Design:

// \Magento\Catalog\Model\Design::_extractSettings() [excerpt]

$settings->setCustomDesign(
    $object->getCustomDesign()
)->setPageLayout(
    $object->getPageLayout()
)->setLayoutUpdates(
    (array)$object->getCustomLayoutUpdate()
);

That pulls the entire string <update handle="my_handle_lorem_ipsum_lorem" /> from the category attribute. Does that mean it's trying to use the entire string as the handle name?

All I see is the call to addUpdate(). There doesn't seem to be a call to any function of the Merge class that would incorporate it into the overall layout XML.

In general, how should we add a handle in the category field "Custom Layout Update," and where and how should the file should be declared?

Best Answer

I'm trying to solve a similar issue, so I'm reading through the code pretty extensively to get to the root of this. I'll start at the end and work backward:

The call to addUpdate() you reference actually does end up calling addUpdate() on the \Magento\Framework\View\Model\Layout\Merge class. Hold onto your seat: it's a bumpy road. The $page variable is an instance of \Magento\Framework\View\Result, and it inherits the definition of addUpdate() from its parent class Layout (in the same namespace). That class in turn calls addUpdate() on a ProcessorInterface, whose default implementation is none other than: Magento\Framework\View\Model\Layout\Merge.

So then to your question about it seeming to pass the entire XML string: you are correct, and the behavior is correct. The Merge class should theoretically be taking care of parsing the <update> inside _fetchRecursiveUpdates() and then loading the layout XML from the referenced handle.

The catch is that _fetchRecursiveUpdates() is never called on XML chunks that are added via addUpdate(). They are assumed to be ready-to-go (non-recursive) layout instructions. Layout XML from external addUpdate() calls are included directly via a call to $this->asString() at the very end of Merge::load(). They never pass through the inner _merge() function which is what indirectly calls _fetchRecursiveUpdates().

A simplistic solution would be to create a new class that extends the built-in Merge class and set it as the preferred implementation of ProcessorInterface. You would then change the addUpdate() method to perform the same logic as _fetchRecursiveUpdates() before saving its argument to the internal array, something like the following:

public function addUpdate($update)
{
    $updateXml = $this->_loadXmlString($update);
    if (strtolower($updateXml->getName()) == 'update' && isset($updateXml['handle'])) {
        $this->_merge((string)$updateXml['handle']);
    } else {
        $this->updates[] = $update;
    }
    return $this;
}