Overriding a Template
A theme copy and a layout template swap both replace a template, and the fallback order and upgrade cost differ.
<referenceBlock name="block.name" template="Vendor_Module::path/file.phtml"/>There are two ways to replace a template, and they are not equivalent.
From a theme
Put a file at the same relative path inside your theme: app/design/frontend/Vendor/theme/Magento_Catalog/templates/product/price/final_price.phtml. The name and path must match the original exactly. Nothing else changes; the file is found instead of the module's copy.
From a module
Point the existing block at your own file in layout XML: <referenceBlock name=".." template="Vendor_Module::my/file.phtml"/>. The original stays where it is and you own a separate file under your module.
Fallback order
- The current theme, at
<theme>/Vendor_Module/templates/ - Each ancestor theme in turn, until a theme with no parent
- The module, at
view/frontend/templates/ - The module, at
view/base/templates/
Upgrade cost
A theme copy is a fork. When an upgrade changes the original, your copy keeps rendering the old markup with no warning, so every upgrade means diffing it by hand. A layout swap has the same problem for the body of the file but is at least visible in your module's layout. The cheapest change is neither: leave the template alone and feed it different data through a view model or a layout argument.
After either change run bin/magento cache:flush. A newly added theme also needs bin/magento setup:upgrade so it is registered.
Example
<!-- app/code/Vendor/Catalog/view/frontend/layout/catalog_product_view.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<!-- Keep the core block and its PHP. Render our own markup. -->
<referenceBlock name="product.attributes"
template="Vendor_Catalog::product/view/attributes.phtml"/>
</body>
</page>When to use it
- Change the markup of the product page attribute list for one theme.
- Replace a core template from a module so the change applies to every theme.
- Work out whether a theme copy or the module file is rendering an element.
- Review which theme templates have drifted from core before an upgrade.
More examples
The theme route, same file in a different place
No layout XML and no module are needed: the fallback finds the theme file first. Use this when the change is genuinely presentational and belongs to one theme's design. The @noEscape annotation is copied from the core file, where the value has already been prepared for output; adding it to a value you introduced yourself is how an XSS hole gets past review.
<?php
// Original, shipped by the module:
// vendor/magento/module-catalog/view/frontend/templates/product/view/attributes.phtml
//
// Override, in your theme. Same relative path, Magento_Catalog prefix:
// app/design/frontend/Vendor/storefront/Magento_Catalog/templates/product/view/attributes.phtml
/** @var Magento\Catalog\Block\Product\View\Attributes $block */
/** @var Magento\Framework\Escaper $escaper */
?>
<?php $attributes = $block->getAdditionalData(); ?>
<?php if ($attributes): ?>
<dl>
<?php foreach ($attributes as $data): ?>
<dt><?= $escaper->escapeHtml($data['label']) ?></dt>
<dd><?= /* @noEscape */ $data['value'] ?></dd>
<?php endforeach; ?>
</dl>
<?php endif; ?>Which file is actually winning
Worth running before and after an override. Two common surprises it settles: the path in the theme is subtly wrong, so the module file is still rendering, and a parent theme already overrides the file you are copying from the module, so your starting point was not the core version at all.
// Terminal, not a file.
bin/magento dev:template-hints:enable
// Template hints enabled.
bin/magento cache:flush
// Flushed the Magento Cache.
// Reload the page: each block is labelled with the resolved template path,
// so you can see whether the theme copy, an ancestor theme's copy or the
// module file was used.
bin/magento dev:template-hints:disable
bin/magento cache:flushThe change that costs nothing at upgrade time
If the core template already reads a view model, or you only need extra data rather than different markup, adding an argument beats copying the file. Nothing is forked, so an upgrade that improves the template improves your store too. Reach for a full override only when the markup itself has to change. Apply it with bin/magento cache:flush.
<!-- app/code/Vendor/Catalog/view/frontend/layout/catalog_product_view.xml -->
<referenceBlock name="product.attributes">
<arguments>
<argument name="view_model" xsi:type="object">Vendor\Catalog\ViewModel\AttributeLabels</argument>
</arguments>
</referenceBlock>
Discussion