View models and blocks
A template should only print data. A separate class prepares it — a view model: it talks to the configuration, the database, other modules and hands ready values to the template. The template stays short, and the logic can be covered by a test.
Where a view model lives
The classes go into the module's ViewModel folder. The file is created by hand.
app/code/Acme/Hello/
├── Controller/Index/Index.php
├── ViewModel/
│ └── Greeting.php new file
├── etc/
└── view/frontend/
├── layout/hello_index_index.xml
└── templates/greeting.phtmlThe class
// app/code/Acme/Hello/ViewModel/Greeting.php
namespace Acme\Hello\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
class Greeting implements ArgumentInterface
{
public function getBenefits(): array
{
return ['Delivery in 2 days', 'Pay on delivery', '30-day returns'];
}
}A view model is a plain class. The only requirement is the ArgumentInterface interface: it has no methods and merely allows the object to be passed into a block.
Attaching it in the layout
<!-- app/code/Acme/Hello/view/frontend/layout/hello_index_index.xml -->
<block name="hello.greeting" template="Acme_Hello::greeting.phtml">
<arguments>
<argument name="view_model" xsi:type="object">Acme\Hello\ViewModel\Greeting</argument>
</arguments>
</block>The object type means that Magento creates an object of the given class itself and passes it to the block under the name view_model.
Using it in the template
<!-- app/code/Acme/Hello/view/frontend/templates/greeting.phtml -->
<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Acme\Hello\ViewModel\Greeting $viewModel */
$viewModel = $block->getData('view_model');
?>
<ul>
<?php foreach ($viewModel->getBenefits() as $benefit): ?>
<li><?= $escaper->escapeHtml($benefit) ?></li>
<?php endforeach; ?>
</ul>Run bin/magento cache:clean and open /hello: a list of three items appears on the page.
Dependencies
A view model lists everything it needs in its constructor — Magento passes the objects in:
// app/code/Acme/Hello/ViewModel/Greeting.php
use Magento\Store\Model\StoreManagerInterface;
public function __construct(private StoreManagerInterface $storeManager) {}
public function getStoreName(): string
{
return (string) $this->storeManager->getStore()->getName();
}The method returns the name of the current store view. In the template it is printed as $escaper->escapeHtml($viewModel->getStoreName()). After changing the constructor, delete the contents of the generated folder.
A custom block class
A block has a class too. If the layout does not name one, Magento\Framework\View\Element\Template is used. Your own class extends it and goes into the Block folder:
// app/code/Acme/Hello/Block/Greeting.php
namespace Acme\Hello\Block;
use Magento\Framework\View\Element\Template;
class Greeting extends Template
{
public function getGreeting(): string
{
return (string) __('Hello!');
}
}In the layout the class is set with the class="Acme\Hello\Block\Greeting" attribute, and in the template the method is called as $block->getGreeting().
| Approach | When to use it |
|---|---|
| View model | Almost always: the class is simple and extends nothing |
| Custom block class | When the behaviour of the block itself must change, for example its caching rules |
Older core modules prepare data in blocks, newer ones in view models.
Common mistake
Logic is written right in the template: a database query or an ObjectManager::getInstance() call. Such code cannot be reused, tested or replaced from another module. Keep only output, conditions and loops in a template and move everything else into a view model.