Blocks
A block is the PHP object behind a template: layout places it, it exposes public methods, and the .phtml calls them.
class Vendor\Module\Block\Name extends \Magento\Framework\View\Element\TemplateA block is a PHP object that produces one region of HTML on a page. Layout XML names the block and attaches a template to it. The block exposes public methods. The template calls those methods. A block class on its own renders nothing: it appears only once a layout file places it somewhere.
The Template block
Magento\Framework\View\Element\Template is the standard block. It reads a template argument and renders that .phtml file. Its own class docblock says "Avoid extending this class" and points at view models instead. Extend it when you need block behaviour such as cache tags or child rendering. Otherwise prefer the generic class plus a view model.
Constructor and context
The constructor is __construct(Template\Context $context, array $data = []). A subclass puts its own typed dependencies between those two parameters and passes both on to parent::__construct(). The context already carries common services, so $this->_scopeConfig, $this->_urlBuilder and $this->getRequest() work without injecting anything. $data holds the layout arguments, read with $this->getData('key').
What trips people up
$block->getChildHtml('alias') renders a child block declared under this one in layout. Omit the call and the child never appears, with no error. After adding the class and the layout file, run bin/magento cache:flush. In production mode also run bin/magento setup:di:compile, because the block is built from compiled dependency injection configuration.
Example
<?php
// app/code/Vendor/Notice/Block/ShippingNotice.php
declare(strict_types=1);
namespace Vendor\Notice\Block;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
use Magento\Store\Model\ScopeInterface;
class ShippingNotice extends Template
{
private PriceCurrencyInterface $priceCurrency;
public function __construct(
Context $context,
PriceCurrencyInterface $priceCurrency,
array $data = []
) {
parent::__construct($context, $data);
$this->priceCurrency = $priceCurrency;
}
public function isEnabled(): bool
{
return $this->_scopeConfig->isSetFlag(
'vendor_notice/general/enabled',
ScopeInterface::SCOPE_STORE
);
}
public function getThreshold(): string
{
$amount = (float)$this->_scopeConfig->getValue(
'vendor_notice/general/threshold',
ScopeInterface::SCOPE_STORE
);
return $this->priceCurrency->format($amount, false);
}
}
// Nothing renders yet. Once layout places this block, the storefront header
// shows one line of text, for example "Free shipping over $50.00".When to use it
- Show a configuration-driven free shipping notice in the storefront header.
- Render a group of child blocks in a chosen order with getChildHtml.
- Reuse one template on three layout handles, passing a different argument each time.
- Expose a formatted price or label to a template without putting logic in the .phtml.
More examples
Placing the block in layout
The default handle applies to every storefront page, so the notice appears site-wide inside the header panel. The name must be unique across the merged layout; another file can later reference or remove vendor.shipping.notice by that name. Adding this file changes nothing until you run bin/magento cache:flush, because the merged layout is cached.
<!-- app/code/Vendor/Notice/view/frontend/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="header.panel">
<block class="Vendor\Notice\Block\ShippingNotice"
name="vendor.shipping.notice"
template="Vendor_Notice::shipping-notice.phtml"/>
</referenceContainer>
</body>
</page>The template that calls the block
The template holds no business logic: it asks the block two questions and prints the answer. The @var docblocks are what give an IDE the block's method list, since $block is typed only as BlockInterface at runtime. Reach for this shape whenever a template needs a value that comes from configuration or the database.
<?php
// app/code/Vendor/Notice/view/frontend/templates/shipping-notice.phtml
/** @var Vendor\Notice\Block\ShippingNotice $block */
/** @var Magento\Framework\Escaper $escaper */
?>
<?php if ($block->isEnabled()): ?>
<p><?= $escaper->escapeHtml(__('Free shipping over %1', $block->getThreshold())) ?></p>
<?php endif; ?>Two spellings of the same constructor
These are the same thing. Promotion declares the property and assigns it from the parameter, so the second form produces an identical object. Magento core still uses the longhand because much of it predates PHP 8, so expect to read both. The promoted parameter must stay between $context and $data: dependency injection matches block arguments by name, but $data is conventionally last.
<?php
// Longhand: declare the property, then assign it in the body.
private PriceCurrencyInterface $priceCurrency;
public function __construct(
Context $context,
PriceCurrencyInterface $priceCurrency,
array $data = []
) {
parent::__construct($context, $data);
$this->priceCurrency = $priceCurrency;
}
// Shorthand: constructor property promotion (PHP 8.0 and later).
public function __construct(
Context $context,
private readonly PriceCurrencyInterface $priceCurrency,
array $data = []
) {
parent::__construct($context, $data);
}
Discussion