phtml Templates
Templates are .phtml files addressed by module reference, handed $block and $escaper, and written with the short echo tag.
<?= $escaper->escapeHtml($block->getMessage()) ?>A template is a .phtml file: HTML with small islands of PHP. Module templates live in app/code/Vendor/Module/view/frontend/templates/ and are referenced as Vendor_Module::folder/file.phtml. That module prefix is not a filesystem path. It is what lets a theme supply its own copy of the same file.
The variables you get
$block is the block the template belongs to, and every method you call on it must be public. $escaper is an instance of Magento\Framework\Escaper, available in templates since Magento 2.4.0; the older $block->escapeHtml() still works but is deprecated. Two more arrive the same way: $secureRenderer, for emitting inline style or script under a content security policy, and $localeFormatter. Anything the block passes with assign() appears as a variable of that name.
Short echo and longhand
<?= $x ?> and <?php echo $x; ?> are the same statement. The short echo tag is always enabled and is not affected by the short_open_tag setting. Magento writes output with the short form and uses <?php ?> only for control flow.
What trips people up
Keep conditionals shallow and move anything resembling a calculation into a view model. After editing a template, run bin/magento cache:clean block_html full_page. In production mode a minified copy is written under var/view_preprocessed, so remove that directory if an edit does not appear.
Example
<?php
// app/code/Vendor/Notice/view/frontend/templates/shipping-notice.phtml
/** @var Vendor\Notice\Block\ShippingNotice $block */
/** @var Magento\Framework\Escaper $escaper */
$threshold = $block->getThreshold();
if ($block->isEnabled() && $threshold !== ''):
<div data-threshold=" $escaper->escapeHtmlAttr($threshold) ">
<p> $escaper->escapeHtml(__('Free shipping over %1', $threshold)) </p>
<a href=" $escaper->escapeUrl($block->getUrl('vendor_notice/terms/index')) ">
$escaper->escapeHtml(__('Terms'))
</a>
</div>
else:
$block->getChildHtml('vendor.notice.fallback')
endif;
// The storefront header shows one line of text and a Terms link.
// getChildHtml renders the named child block only when it is reached. When to use it
- Render a block's data as markup without putting queries in the file.
- Read a core template to find the method name you need to change.
- Trace which of several templates produced an element on the page.
- Add a link or an attribute to existing markup using the correct escaper method.
More examples
Short echo next to longhand
Magento core uses the short echo tag for output and the colon form for control flow, because the colon form makes a long block of HTML easier to match up with its closing tag. The brace form is identical to PHP. Recognising both matters when you read a third party module that follows other conventions.
<?php // Both lines emit the same escaped string. ?>
<?= $escaper->escapeHtml($block->getLabel()) ?>
<?php echo $escaper->escapeHtml($block->getLabel()); ?>
<?php // Both loops are the same loop, written with braces or with endforeach. ?>
<?php foreach ($block->getItems() as $item) { ?>
<li><?= $escaper->escapeHtml($item->getName()) ?></li>
<?php } ?>
<?php foreach ($block->getItems() as $item): ?>
<li><?= $escaper->escapeHtml($item->getName()) ?></li>
<?php endforeach; ?>Passing values from a block with assign()
assign() adds a value to the block's view variables, which the template engine extracts into local variables before including the file. It is worth knowing because core templates use it, but a getter on the block or a view model is easier to follow: a variable that appears from nowhere is hard to trace. Note that extraction skips names already in scope, so assign('block', ...) cannot overwrite $block.
<?php
// app/code/Vendor/Notice/Block/Banner.php (excerpt)
declare(strict_types=1);
protected function _prepareLayout()
{
$this->assign('bannerLabel', __('Autumn sale'));
return parent::_prepareLayout();
}
// app/code/Vendor/Notice/view/frontend/templates/banner.phtml
// $bannerLabel exists here because assign() put it in the view variables.
?>
<p><?= $escaper->escapeHtml($bannerLabel) ?></p>Finding which template rendered what
Template hints are the fastest way to answer which file produced a piece of markup, which matters before you override anything. The command writes dev/debug/template_hints_storefront into core_config_data, so a cache flush is needed for it to show, and the command description says so. Turn hints off before anyone else sees the site.
// Terminal, not a file.
bin/magento dev:template-hints:enable
// Template hints enabled.
bin/magento cache:flush
// Flushed the Magento Cache.
// Then reload the storefront: every block is outlined and labelled with its
// template path and block class.
bin/magento dev:template-hints:disable
bin/magento cache:flush
Discussion