Türchen 7: Better Blocks: Magento 2 PHP View Models

Dec
7
2017

Magento 2 Blocks

Since the very beginning of Magento 1, all server side view layer logic was encapsulated in Block classes. This approach has not changed in Magento 2 (so far).

Last week I had the chance to talk to Anton Kril of Magento and he told me I should consider switching to a new way of providing view layer logic instead of using blocks: PHP View Models.

About blocks

I believe we all probably agree that templates should only be about representation, and should not contain business logic.
The view level business logic is what blocks are for. At least for me, the most common type of logic in blocks is to provide data to the template.

Block classes have to extend from \Magento\Framework\View\Element\AbstractBlock, which in turn implements \Magento\Framework\View\Element\BlockInterface.
Since most custom blocks require rendering a template, in most cases they actually inherit from \Magento\Framework\View\Element\Template, one step further up the inheritance chain.

Problems with blocks

While blocks do their job just fine, they do have some shortcomings:

All blocks use constructor injection, so when a custom block requires additional dependencies, it has to pass the transient Context dependency along to the parent::__construct() method.

Naturally, many developers then access the parents dependencies through protected getters or even protected fields.
This interweaving of custom logic and platform code leads down the path of complexity.
Code becomes harder to understand and maintain when compared to plain classes without inheritance.

Also, when using tests to drive the development of blocks, it is quite cumbersome having to deal with the parent class dependencies, even though they actually are not related to custom business logic.
This is an additional obstacle to developers starting out on the TDD journey and facilitates the myth that testing is hard.

PHP view models

While writing this post I just discovered the following PHPDoc comment above the template block class:

/**
 * Avoid extending this class.
 *
 * If you need custom presentation logic in your blocks, use this class as block, and declare
 * custom view models in block arguments in layout handle file.
 *
 * Example:
 * <block name="my.block" class="Magento\Backend\Block\Template" template="My_Module::template.phtml" >
 *     <arguments>
 *         <argument name="viewModel" xsi:type="object">My\Module\ViewModel\Custom</argument>
 *     </arguments>
 * </block>
 **/

This comment was added in the 2.2 release and describes exactly what Anton was referring to.

Even better, since Magento 2.2.1 we don’t even have to specify the class argument on the <block/> node, since class="Magento\Framework\View\Element\Template" is the default anyway.

I now declare most of my blocks in layout XML like this:

<referenceContainer name="columns.top">
    <block name="view-model-example" template="Example_ViewModel::example.phtml">
        <arguments>
            <argument name="view_model"
                      xsi:type="object">Example\ViewModel\Block\Example</argument>
        </arguments>
    </block>
</referenceContainer>

The templates begin like this:

<?php declare(strict_types=1);

/** @var \Example\ViewModel\Block\Example $viewModel */
$viewModel = $block->getData('view_model');

?>

<?= $viewModel->getSomeThing() ?>

The view models have to implement a marker interface, or we end up with an exception:

(UnexpectedValueException): Instance of Magento\Framework\View\Element\Block\ArgumentInterface is expected, got Example\ViewModel\Block\Example instead.

This is the ArgumentInterface this exception refers to:

/**
* Block argument interface.
* All objects that are injected to block arguments should implement this interface.
*/
interface ArgumentInterface
{
}

This restriction is a security precaution. Since layout can be configured from multiple places – including the adminhtml by the merchant – it serves to limit the number of blocks that can be instantiated by layout instructions.

Having to implement this interface is not a real inconvenience in practice. I enjoy working with almost decoupled view models more than blocks extending from AbstractBlock.

<?php declare(strict_types=1);

namespace Example\ViewModel\Block;

use Magento\Framework\View\Element\Block\ArgumentInterface;

class Example implements ArgumentInterface
{
    public function getSomeThing()
    {
        // ...
    }
}

A constructor does not need to call parent::__construct(), and any dependencies are plain to see.

There still are use some cases where using a custom block is required. Choosing the template to render conditionally for example. But in most cases view models are good enough.

Benefits of view models

In a nutshell, using view models instead of blocks provides a better separation of custom and platform code, which leads to code with the following properties:

  • easier to understand
  • more maintainable
  • more reusable
  • more upgrade safe
  • simpler to test

Happy Christmas and happy coding!

Comments

Kommentieren

Your email address will not be published. Required fields are marked *