Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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.phtml

The 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().

ApproachWhen to use it
View modelAlmost always: the class is simple and extends nothing
Custom block classWhen 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.

Thanks for reading!

Donate to the site — we work hard for you.