Magento 2.2.0 and 2.2.1 made some interesting changes to the layout system, with the goal to make the inheritance based block system redundant:
- all blocks are automatically considered of the "Template" type if not specified otherwise
- classes can be injected via layout XML
What does that implicate? Previously, if you needed custom functionality in a block, you created a custom block class, extended from the default template block and added methods and dependencies as needed. Why is that a bad thing?
First of all, it's an inheritance based API and it's a good old principle of object oriented design to favor composition over inheritance. In Magento 2 the problems of inheritance are evident whenever the constructor signature of the base class changes. With the introduction of context objects that contain the common dependencies for blocks, controllers or models, it happens less, but still more often than not. In that case all children would break, as soon as they override the constructor. And since we have constructor dependency injection, this is the case almost always.
Second, the methods are coupled to that particular block. You can reuse the block in different places with different templates, or even extend it (inheritance...) but there's no way to share functionality across different blocks without duplicating code or delegating to helpers (helpers...)
Now what can we do instead? Every method you would implement in a block, goes into a view model instead. The view model does not extend from anything. If it needs access to methods of the abstract block class, there are two cases to distinguish:
- If the method in the abstract block delegates to something else (like
getUrl()
to the URL model orescapeHtml()
to the Escaper), use that dependency directly by requesting it in the constructor. - if the method operates on the block itself and the layout (like
getParent()
), add the block as method parameter and pass$block
from the template.- Special case: if it is using only
getData()
or a magic getter, take that data as parameter, you don't need the whole block
- Special case: if it is using only
The view model is injected into the block by layout XML as follows:
<block template="IntegerNet_MyModule::my_template.phtml"> <arguments> <argument name="myViewModel" xsi:type="object">IntegerNet\MyModule\MyViewModel</argument> </arguments> </block>
And in the template you can access it with the name you gave it (here: myViewModel)
/** @var \IntegerNet\MyModule\MyViewModel $myViewModel */ $myViewModel = $block->getData('myViewModel');
This is basically dependency injection, but without you having to write a custom block class for it. The view model MUST implement \Magento\Framework\View\Element\Block\ArgumentInterface
, which is a marker interface without any methods. The reason behind this is to prevent you from misusing the system and for example inject sessions, repositories and any other classes to then write business logic in the template. Instead, a conscious decision should be made to define a class as view model.
The concept already has been introduced in other blog posts (e.g. Jisse Reitsma: View Models in Magento 2, Vinai Kopp: Better Blocks in Magento 2: PHP View Models), which I recommend to read, but here it does not stop!
With the new possibilities, we don't have to map blocks 1:1 on view models. Instead we can inject as many view models as we want. This results in better structured and more reusable code, since we can have lots of small view models with a single responsibility. Many will probably just have one method. This should even be the standard if the methods don't share any state, i.e. use the same properties, or if they are semantically coupled, i.e. have the same reason to change. For that reason, I recommend not to name the argument "viewModel", since it is not THE view model, just A view model, with a specific purpose. As such, it deserves a less generic name.
Side note: You don't have to be religious about this or you might end up with the other extreme:
Too many ViewModels is bad too. I've already created a Block with 9 ViewModels and got lost on where the data came from 😉
— Jisse Reitsma (@jissereitsma) 4. April 2018
If a block has 9 view models, it's probably time to refactor. Maybe multiple view models can be composed? Maybe there is still too much logic in the template so that it needs to pull data from different sources to combine it into output?
How to design a good view model
What are the properties of a good view model?- Single responsibility: Since you are not restricted to one view model per block, design them as coherent as possible and do not mix responsibilities. This makes them easy to reuse and easy to replace.
- Minimal dependencies: You already get a reduced number of dependencies as a side effect from "single responsibility" Additionally to constructor dependencies, try to minimize the dependencies from method arguments. Take only the minimal needed data, e.g. dynamic block parameters if you need them, instead of the whole block.
Bad:
public function formatTitle($block) { return strtoupper($block->getTitle(); }
Good:
public function formatTitle($title) { return strtoupper($title); }
This also makes unit tests for view models viable, a huge improvement compared to blocks.
- As always in Magento 2, use interfaces as dependencies instead of concrete classes wherever possible.
- As always in Magento 2, do not add any logic in the constructor, only assignments.
- If view models operate on a single model (e.g. the product, to format product data), consider decorators (I'll explain those in another blog post).
Hey, you said "and Magento 1"
If you are working with Magento 1, you can port this concept by passing a factory method to the block, which then creates the view model. There is a lesser used layout feature which comes in handy here: the "helper" attribute for method arguments:<action method="setMyViewModel"> <model helper="mymodule/factory::createMyViewModel" /> </action>This translates to the following PHP code:
$block->setMyViewModel( Mage::helper("mymodule/factory")->createMyViewModel() );And all you need is a factory helper with one method per view model:
class IntegerNet_MyModule_Helper_Factory { public function createMyViewModel() { return Mage::getModel("mymodule/myviewmodel"); } public function createAnotherViewModel() { return Mage::getModel("mymodule/anotherviewmodel"); } }And then you can use the view model in the template:
$myViewModel = $this->getMyViewModel();To be fair, nothing stops you from using
$myViewModel = Mage::getModel("mymodule/myviewmodel")
directly in the template, if you want, but Mage::getModel()
in templates was considered bad practice even in Magento 1.
Everything else can be applied the same way as described above.
I myself really like the concept and that Magento makes it easier to apply with the recent changes. No more custom block classes! If you want to see a real example, look at the recent hackathon project Firegento_DevDashboard where we did not write any custom block classes.