With Magento 2.2 on the roadmap for September, various new improvements popped up on the radar. One of these is the concept of ViewModels, offloading features from Block classes into separate ViewModel classes. But do we need to wait until Magento 2.2 before we can use these ViewModels?
Using ViewModels in Magento 2.2
Using ViewModels in Magento 2.2 is pretty cool: You just add an additional XML argument to the Block class - either when creating the block using <block>
or when referring to the block using <referenceBlock>
. This might look like the following:
<block class="Yireo\Example\Block\Dummy" name="dummy">
<arguments>
<argument name="view_model" xsi:type="object">Yireo\Example\ViewModel\Dummy</argument>
</arguments>
</block>
The newly created class could be used as follows:
namespace Yireo\Example\ViewModel;
class Dummy implements \Magento\Framework\View\Element\Block\ArgumentInterface
{
public function __construct()
{
}
}
In this case, the constructor is still empty. But because the Object Manager is used to instantiate this class, you can modify the constructor to inject new dependencies as you are used to in other classes. You can apply all of the coolness of Magento DI here.
Finally, you can insert the new ViewModel class in your existing PHTML template as follows:
$viewModel = $block->getViewModel();
Composition over inheritance
Ok, this is nice. But what's the point?
As you might know, most Block classes that exist in Magento 2 are created by extending upon the \Magento\Framework\View\Element\Template
class. This parent class brings template functionality, plus a lot of other handy things like references to the current layout, a logger and so on.
Usually, when you create a Block class for your own module, you can reuse a lot of functionality from the parent. But as your class grows in functionality, you might need to insert other dependencies into your Block class. And the way to do that is to override the PHP constructor and add your own dependencies to it. To make sure your parent constructor still works, you need to duplicate all of the parent dependencies and pass them on to the parent like follows:
namespace Yireo\Example\Block;
class Dummy extends \Magento\Framework\View\Element\Template
{
public function __construct(
\Magento\Backend\Block\Template\Context $context,
array $data = []
) {
parent::__construct($context, $data);
}
}
This starts to clutter your code. You need to add more code to a PHP constructor, only to satisfy the parent. If there was no parent at all, you would not need to add $context
or $data
to your class in the first place. Inheritance is working against us, making a simple class more complex than we want. We are not only maintaining our class anymore, we are alo maintaining the stuff that lives in our parents.
ViewModels are there to simplify things again. Instead of inserting your dependency in the Block class, you add it to the ViewModel class instead. And while you would normally reference $block->getSomething()
to get the end-result of that dependency from your class into your template, you now use $viewModel->getSomething()
.
ViewModels make things SRP-compliant
If you are familiar with the code principle SOLID, then you know that each class should ideally have only one purpose to change - this is known as SRP (Single Responsibility Principle). In other words, if a class does multiple things, it is bad. Because a Block class has the responsibility to output a template, it needs to extend from the parent class \Magento\Framework\View\Element\Template
. Anything else that you add to the Block class, gets you further away from SOLID code.
By using ViewModels, you can keep your Block classes tiny. Theoretically, you can get rid of them and simply use a generic class instead:
<block class="Magento\Framework\View\Element\Template" name="dummy">
<arguments>
<argument name="view_model" xsi:type="object">Yireo\Example\ViewModel\Dummy</argument>
</arguments>
</block>
This allows you to disconnect your code from the Magento parents and focus on the data that you actually need to insert into your PHTML-templates: The main purpose of ViewModels is to output data (modelling) for availability in the template (view).
$context is an anti-pattern
And to make sure you really get the point: Every Block-class inserts itself with a $context
object, needed by the parent classes. The $context
object is used as a wrapper for a whole bunch of dependencies. While your constructor only needs to duplicate a few lines of code to satisfy the parent constructor, this is hiding the fact that the $context
brings in a lot of dependencies that your ViewModel logic does not need.
I'm not diving into the discussion on why $context
is bad and why enforcing it (as is being done in Magento 2.1) is even worse. Fact is that ViewModels allow you to get rid of $context
, because your ViewModel does not need any parent.
Also in Magento 2.0 or 2.1?
So far so good. We can use ViewModels in Magento 2.2. Nice. But what about older versions of Magento? I'm writing this with a couple of sites (and extensions) running on 2.1 and basically, I want to enjoy the removal of $context
from my classes now already. Can this be done?
The bad news is that the XML layout example above injected the new data variable view_model
as an object (xsi:type="object"
) and this XSI type is not available in Magento 2.0 or 2.1. In short, using this XML layout statement under Magento 2.1 will give you a weird error.
I've experimented a little with this. For instance, using an xsi:type="string"
works, allowing me to get the name of the class within my Block class using a simple call $viewModelName = $this->getViewModel()
. But a simple $viewModel = new $viewModelName()
is not what I want, because then I would be missing out on all of the cool DI features of the ObjectManager. And inserting the ObjectManager in a Block class is a big NO. So when are we allowed to use the ObjectManager? Well, when we stick to the Factory pattern.
Creating a ViewModel factory
I've applied this principle now to our Yireo GoogleTagManager2 extension. I created a ViewModel factory that looks like this:
namespace Yireo\Example\Factory;
class ViewModelFactory
{
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager
)
{
$this->objectManager = $objectManager;
}
public function create($block)
{
$viewModelClass = str_replace('\Block\\', '\ViewModel\\', get_class($block));
if (class_exists($className)) {
return $this->objectManager->create($className);
}
}
}
Note that I've simply created my own Factory class. I did not use Magento to generate it for me, while theoretically I could also use the autogenerated version. I didn't, because the code above allowed me to add more tricks to the factory. Remember that a Factory in Magento 2 is simply a class following the factory design pattern, which legalizes the ability to inject the ObjectManager.
Next, in a Block class, I can call upon this factory by simply injecting myself with this factory and passing the full class name of the ViewModel to its create()
method. Because, in my case, every ViewModel has a 1-on-1 relation with a single Block class, I can simply use the Block classname to get the right ViewModel classname:
public function getViewModel()
{
return $this->viewModelFactory->create($this);
}
It works. It allows me to put this generic method in a generic Block and then use that Block to create new Blocks with ViewModel behaviour. It is a bit more work than you will have with the code under Magento 2.2, but it at least got me started with cleaning up Blocks and creating ViewModels.
When to use ViewModels?
When would you want to use ViewModels? Well, when Magento 2.2 comes out and your code doesn't need to be backwards compatible with Magento 2.1 any more, my answer will be: Always! However, reality is that we most likely need to maintain backwards compatibility for some time. And the procedure above is a bit lengthy to follow if Block classes are still simple enough without any custom constructor.
I would say that you create some kind of ViewModel functionality as soon as you start overriding the Block constructor to inject your own dependency into it. A Block class that extends from a parent and simply reuses the parents dependencies is not that bad, as long as you don't have a huge dislike of $context
. However, as soon as the Block becomes more complex, it's always better offload features to other classes. And ViewModels are meant to be the good addition to that.
About the author
Jisse Reitsma is the founder of Yireo, extension developer, developer trainer and 3x Magento Master. His passion is for technology and open source. And he loves talking as well.