Debugging Layout

Template hints, the applied handle list and the merged layout dump answer which file put a block on the page.

Syntaxbin/magento dev:template-hints:enable && bin/magento cache:flush

Layout problems are rarely syntax. The questions are which handles applied, and which file put this block here. Three tools answer them.

Template hints

Run bin/magento dev:template-hints:enable, then bin/magento cache:flush. Every block rendered through a template is outlined with its template path. dev:template-hints:disable turns it off and dev:template-hints:status reports the state. Block names appear as well when Stores > Configuration > Advanced > Developer > Debug > Add Block Names to Hints is set to Yes, which is the configuration path dev/debug/template_hints_blocks. Containers have no template, so they never produce a hint.

Handles and the merged tree

The event layout_generate_blocks_before hands you layout and full_action_name. From there, $layout->getUpdate()->getHandles() is the list of handles that applied, and $layout->getUpdate()->asSimplexml()->asXML() is the whole merged document. Search that dump for the block name you want gone.

Finding the module

Once you have the name, grep for it:

grep -rn 'name="product.info.mailto"' app/code vendor/magento/*/view app/design

What trips people up

The CLI command writes the storefront setting at default scope. A value saved earlier for one store view still wins, and nothing changes until the cache is flushed. Hints also honour dev/restrict/allow_ips: if that list does not include your address, hints stay off with no message.

Example

Example · php
<?php
// app/code/Acme/Debug/Observer/LogLayoutInfo.php
declare(strict_types=1);

namespace Acme\Debug\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\View\LayoutInterface;
use Psr\Log\LoggerInterface;

class LogLayoutInfo implements ObserverInterface
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function execute(Observer $observer): void
    {
        /** @var LayoutInterface $layout */
        $layout = $observer->getData('layout');
        $update = $layout->getUpdate();

        $this->logger->debug('Action: ' . (string)$observer->getData('full_action_name'));
        $this->logger->debug('Handles: ' . implode(', ', $update->getHandles()));
        $this->logger->debug('Merged layout: ' . $update->asSimplexml()->asXML());
    }
}

When to use it

  • Find which module added a block you have been asked to remove.
  • Confirm a custom handle actually applied to the page you are testing.
  • Check whether your layout file was merged at all after a cache flush.
  • Identify the template behind a piece of markup you need to change.

More examples

Registering the observer for the storefront only

The file sits under etc/frontend, so the observer runs on the storefront and not in the admin. Load any storefront page and var/log/debug.log gains three lines: the full action name, the applied handles in order, and the merged layout XML. Run bin/magento setup:upgrade for a new module, then bin/magento cache:flush, and delete the observer before the change reaches production.

Example · xml
<!-- app/code/Acme/Debug/etc/frontend/events.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="layout_generate_blocks_before">
        <observer name="acme_debug_log_layout_info" instance="Acme\Debug\Observer\LogLayoutInfo"/>
    </event>
</config>

The commands, and what they print

cache:flush with no arguments clears every cache type; naming the three that hold rendered output is faster on a large store. The enable command only writes the storefront setting, so hints in the admin are a separate configuration field. Check status before you spend time wondering why the outlines are missing.

Example · bash
bin/magento dev:template-hints:enable
bin/magento cache:flush layout block_html full_page

# Template hints enabled.
# Flushed cache types:
# layout
# block_html
# full_page

bin/magento dev:template-hints:status
bin/magento dev:template-hints:disable

The same constructor, both ways

These are the same code: promotion declares the property, types it and assigns it in one place. Magento core uses the longhand almost everywhere, so you will read both forms in the same project. Promotion is the only form where readonly and the declaration sit together, which is why newer modules prefer it for injected dependencies.

Example · php
<?php
// longhand: typed property plus assignment
private LoggerInterface $logger;

public function __construct(LoggerInterface $logger)
{
    $this->logger = $logger;
}

// shorthand: constructor property promotion (PHP 8.0 and later)
public function __construct(private readonly LoggerInterface $logger)
{
}

Discussion

  • Be the first to comment on this lesson.