Inheritance and Fallback

How a view file is resolved through the parent theme chain, and why you override one file instead of forking a theme.

Syntax<theme_dir>/<Vendor>_<Module>/templates/... → <parent_theme_dir>/<Vendor>_<Module>/templates/... → <module_dir>/view/<area>/templates/...

A theme does not need to contain every file. It names a parent in theme.xml, and anything it does not define is taken from that parent, then from the parent's parent, and finally from the module that owns the file. Magento/luma is built exactly this way: it declares Magento/blank as its parent and replaces a subset of files.

The fallback chain

For a template, Magento looks in <theme_dir>/<Vendor>_<Module>/templates, then the same path in each ancestor theme, then <module_dir>/view/frontend/templates. For an asset such as a stylesheet, image or font it looks in <theme_dir>/web and <theme_dir>/<Vendor>_<Module>/web, then the ancestors, then <module_dir>/view/<area>/web, and last the shared library in lib/web. The first file found wins; nothing is merged.

Layout is the exception

Layout XML of the same name is merged, not replaced, so your file extends the module's. To replace one outright, put it in <Vendor>_<Module>/layout/override/base/ to replace the module's file, or in <Vendor>_<Module>/layout/override/theme/<Parent_Vendor>/<parent_theme>/ to replace a parent theme's file.

What trips people up

To change one block of markup, copy that one .phtml into your theme and nothing else. Copying a whole theme directory gives you hundreds of files you now maintain. After adding, moving or deleting a template or layout file, run bin/magento cache:flush; both are cached, and a theme with no cache flush looks like a theme that did not load.

Example

Example · php
<?php
// app/design/frontend/Acme/storefront/Magento_Theme/templates/html/copyright.phtml

declare(strict_types=1);

/** @var \Magento\Theme\Block\Html\Footer $block */
/** @var \Magento\Framework\Escaper $escaper */
?>
<small class="copyright">
    <span><?= $escaper->escapeHtml($block->getCopyright()) ?></span>
    <a href="<?= $escaper->escapeUrl($block->getUrl('contact')) ?>">
        <?= $escaper->escapeHtml(__('Contact us')) ?>
    </a>
</small>

<?php
// The block that renders this file is declared in
// Magento_Theme/view/frontend/layout/default.xml as
// <block class="Magento\Theme\Block\Html\Footer" name="copyright"
//        template="Magento_Theme::html/copyright.phtml"/>
//
// Because the path under the theme matches the module path, fallback finds
// this file first and the module's own copyright.phtml is never used.
//
// On the storefront: every page footer now shows the copyright line plus a
// link to /contact. Run bin/magento cache:flush to see it.

When to use it

  • Change the footer markup without editing anything in vendor/magento.
  • Replace one of Luma's layout files while keeping the rest of Luma.
  • Find out which theme a rendered template actually came from before editing it.
  • Build a child theme that differs from its parent in three files.

More examples

Replacing a parent theme's layout file

A layout file of the same name normally merges with what came before. When the parent theme's file is wrong for you and you want it ignored entirely, put your version under layout/override/theme/<Parent_Vendor>/<parent_theme>/. Use override/base/ instead to ignore the module's own file. This is a blunt instrument: the file you replace stops receiving upstream fixes, so prefer a merged file that changes only what you need. Run bin/magento cache:flush after adding it.

Example · xml
<!-- app/design/frontend/Acme/storefront/Magento_Catalog/layout/override/theme/Magento/luma/catalog_product_view.xml -->
<!-- An override must name a file the ancestor theme really has. Luma ships
     Magento_Catalog/layout/catalog_product_view.xml, so this file replaces it.
     Pointing an override at a file that theme does not have (Luma has no
     Magento_Catalog/layout/catalog_category_view.xml, for instance) fails layout
     loading with "Overriding view file ... does not match to any of the files." -->
<page layout="1column" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <!-- Luma's own moves, kept; its product.info.review move is dropped -->
        <move element="page.main.title" destination="product.info.main" before="-"/>
        <move element="product.info.stock.sku" destination="product.info.price" after="product.price.final"/>

        <referenceContainer name="content">
            <block class="Magento\Framework\View\Element\Template"
                   name="acme.product.notice"
                   before="-"
                   template="Acme_Storefront::product/notice.phtml"/>
        </referenceContainer>
    </body>
</page>

Extending instead of overriding

The same file name without the override directory is merged on top of the module's and the parent theme's versions, so you write only the difference. This is the form to reach for first: it survives upgrades because the original file keeps being loaded. Here the theme adds one block to the footer and leaves the rest of default.xml alone.

Example · xml
<!-- app/design/frontend/Acme/storefront/Magento_Theme/layout/default.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="footer">
            <block class="Magento\Framework\View\Element\Template"
                   name="acme.footer.payments"
                   after="copyright"
                   template="Acme_Storefront::html/payments.phtml"/>
        </referenceContainer>
    </body>
</page>

Seeing which file won

Before you copy a template, confirm which one is being rendered. Template hints print the path of every template on the page, including the theme it was resolved from, so you can tell a module file from a Luma file from your own. Disable them again before anyone else sees the storefront.

Example · bash
# Show the resolved path of every template on the storefront
bin/magento dev:template-hints:enable
bin/magento cache:flush

# ... load the page, read the paths, then:
bin/magento dev:template-hints:disable
bin/magento cache:flush

Discussion

  • Be the first to comment on this lesson.