Quantcast
Channel: Giel Berkers dot com
Viewing all articles
Browse latest Browse all 41

Responsible Templating in Magento 2

$
0
0

Just because you can do anything in a template doesn’t mean you should.

Some of you might already know the S.O.L.I.D way of programming. If you haven’t, it’s really interesting stuff if you want to be a better programmer. And besides that, the S.O.L.I.D principles can be found throughout Magento 2.
The S in S.O.L.I.D stands for Single Responsibility: an object, class or method should only have one responsibility, therefore you must keep it as simple as possible. So let’s just look at how this applies to templates in Magento 2.
If we look at the responsibility of a template, we can simply state:

A template should only be responsible for rendering variables into HTML.
Now take a step back and let that sink in. What does this actually mean? In my opinion,
“Rendering variables into HTML” means:

  • Putting the value of a variable inside a HTML tag / attribute.
  • Rendering (or leaving out) parts of the template according to a boolean condition.
  • Iterating through an iterable variable.

What is doesn’t mean is:

  • Performing complex (PHP) logic.
  • Performing complex Magento tasks, like filtering a product collection for example.
  • Querying database tables (believe me, I’ve seen templates performing 100+ lines of SQL queries without outputting a single character of HTML).

What I like to do is keep my templates as simple as possible. That means we need to minimize the responsibility of logic and decision making in the template, and only use templates for what they are intended to do: render variables into HTML. All logic needs to come from their corresponding block class.

How to become a responsible template developer

When you take a critical look at how templates are handled within Magento 2, you’ll quickly note that templates are ‘too smart’. So what does that mean? When is a template too smart? In my opinion this means that templates are made responsible for certain domain logic or decision making that’s outside the scope of a template.
Take the following template code for example. This is a typical something that you might encounter in a Magento template:

< ?php /** @var \Magento\Framework\View\Element\Template $block */ ?>
< ?php $config = $this->helper('Vendor\Module\Helper\Config'); ?>
< ?php $product = $this->helper('Magento\Catalog\Helper\Data')->getProduct(); ?>
<div> ... some html ... </div>
< ?php if ($config->getConfig('vendor/module/enabled')): ?>
    <ul>
    < ?php $collection = $product->getCollection()
        ->addFieldToFilter('status', $config->getConfig('vendor/module/status_filter')); ?>
    < ?php foreach($_collection as $item) : ?>
        <li>< ?php $item->getName(); ?></li>
    < ?php endforeach; ?>
    </ul>
< ?php endif; ?>
<div> ... some html ... </div>

If you take a critical look at this template you can see that this tiny template is responsible for the following actions:

  • Load Dependency Vendor\Module\Helper\Config.
  • Load Dependency Magento\Catalog\Helper\Data.
  • Load (get) the product object.
  • Render some regular HTML.
  • Decide whether or not it should render an unordered list.
  • Create a collection.
  • Filter the collection.
  • Decide what kind of filter is used for the collection.
  • Iterate through the collection.
  • Render the name of each element in the collection.

Why is this a bad thing?

We see that even in the tiny example above there is a lot of domain logic and decision making going on. This could become even more when a template grows in size. We’ve all seen those templates in Magento that have more than 100 or 200 lines of HTML and PHP code. Bottom line: Templates in Magento are some of the things that can become very complex very quickly.
This is bad because (but not limited to) the following reasons:

  • It makes it easy to introduce bugs.
  • It’s hard to maintain.
  • It’s almost impossible to reuse.
  • It’s difficult to test.

And perhaps most important: you cannot expect from every Magento Frontend developer to have such deep knowledge in either Magento or PHP to pull this of.
Let’s imagine you’re working in a team with 2 developers:

  1. A Magento backend developer that knows how to get things done in Magento. He knows the basic principles of dependency injection, plugins, events, the layout model, etc.
  2. A frontend developer (not limited to Magento) who knows his way into templating, but is not that familiar with all the backend coding stuff.

In theory, they’re a perfect match: The backend developer knows how to work with models, collections, repositories, factories, registries, etc. But the frontend developer doesn’t have all that knowledge. On the other hand, he’s a wizard when it comes to CSS, JavaScript, SVG, animations, you name it!
In this setup, it would make no sense for the frontend developer to know all the inner workings and gotchas of Magento. For example, you cannot expect from every (Magento) frontend developer to perfectly filter a collection with 3 joins to other entities determined on what customer group the current customer is mapped to (and internally cache the results).
But even more important: even if he could write the above use case, doesn’t mean he should do it. It would cause a lot of clutter in the template that makes it harder to read and harder to maintain.

So what’s a frontend developer allowed to do?

In my opinion, templates should only be allowed to have the following PHP functionality:

  • echo
  • for
  • foreach
  • if / else

And the following Magento functionality:

  • __()
  • $block->...() (methods provided by the block class).
  • $block->getChildHtml() (and occasionally some other layout-specific tasks, but only when necessary).

The main idea is that you have a backend developer that writes a block class to provide all information that is required by the template. The template itself should never contain domain logic. So let’s take a look back at the above template, but now write it with everything we know so far:

< ?php /** @var \Vendor\Module\Block\Example $block */ ?>
< ?php $config = $this->helper('Vendor\Module\Helper\Config'); ?>
<div> ... some html ... </div>
< ?php if ($config->getConfig('vendor/module/enabled')): ?>
    <ul>
    < ?php foreach($block->getFilteredCollection() as $item) : ?>
        <li>< ?php $item->getName(); ?></li>
    < ?php endforeach; ?>
    </ul>
< ?php endif; ?>
<div> ... some html ... </div>

Now if we look at this template you can see that this tiny template is now a bit less responsible than before. It’s now only responsible for:

  • Load Dependency Vendor\Module\Helper\Config.
  • Render some regular HTML.
  • Decide whether or not it should render an unordered list.
  • Iterate through a collection provided by the block.
  • Render the name of each element in the collection.

As you can see, we let our $block-class return a filtered collection, so we no longer have to create and filter the collection in our template. It’s no longer responsible for that. Also note that our block class is no longer the default Magento\Framework\View\Element\Template, but we created a new class Vendor\Module\Block\Example to provide the functionality for our template:

< ?php

namespace Vendor\Module\Block;

class Example extends \Magento\Framework\View\Element\Template
{
    /**
     * @var \Magento\Catalog\Helper\Data
     */
    protected $productDataHelper;

    /**
     * Example constructor.
     * @param \Magento\Catalog\Helper\Data $productDataHelper
     * @param \Magento\Framework\View\Element\Template\Context $context
     * @param array $data
     */
    public function __construct(
        \Magento\Catalog\Helper\Data $productDataHelper,
        \Magento\Framework\View\Element\Template\Context $context,
        array $data = []
    ) {
        $this->productDataHelper = $productDataHelper;
        parent::__construct($context, $data);
    }

    /**
     * @return \Magento\Catalog\Model\ResourceModel\Product\Collection
     */
    public function getFilteredCollection()
    {
        $collection = $this->productDataHelper
            ->getProduct()
            ->getCollection()
            ->addFieldToFilter(
                'status',
                $this->_scopeConfig->getValue('vendor/module/status_filter')
            );

        return $collection;
    }
}

As you can see, we’ve now already moved a lot of domain logic out of our template and into our block class. However, there is still some decision making inside of our template. This brings me to my next topic:

Conditional statements

In our template we have the following line:

< ?php if ($config->getConfig('vendor/module/enabled')): ?>

Believe it or not, but this small configuration check is actually domain logic that has seeped in into our template. At this point in our project the decision to render or not is only decided by a configuration setting. But that’s domain logic! The template should not set the condition if it should render or not, it should only
need to know if it must render. The following would make much more sense:

< ?php if ($block->isEnabled()) : ?>

Because now the template asks it’s block class if it is enabled. The block class in it’s turn can determine the conditions that set this flag.
This might seem like a bit overhead, but any condition you put in your template can quickly become domain logic. Take the following examples:

< ?php if ($block->getStatus === \Vendor\Module\Model\Config\Source\Status::DONE) : ?>
< ?php if ($block->getCollection()->count() > 10) : ?>
< ?php if (!empty($block->getProduct->getLabel()) : ?>

vs:

< ?php if ($block->isStatusDone()) : ?>
< ?php if ($block->isCollectionTooBig()) : ?>
< ?php if ($block->hasProductLabel()) : ?>

The list goes on… The most important part to see here is that all conditions are moved outside of the template. The template only needs to know boolean flags, not the conditions that determine them.
All with all, our tiny template could now look something like this:

< ?php /** @var \Vendor\Module\Block\Example $block */ ?>
<div> ... some html ... </div>
< ?php if ($block->isEnabled()): ?>
    <ul>
    < ?php foreach($block->getFilteredCollection() as $item) : ?>
        <li>< ?php $item->getName(); ?></li>
    < ?php endforeach; ?>
    </ul>
< ?php endif; ?>
<div> ... some html ... </div>

Foreach loops

Another thing you could consider simplifying are the use of foreach-loops in your templates. In the above example we have:

<ul>
< ?php foreach($block->getFilteredCollection() as $item) : ?>
    <li>< ?php $item->getName(); ?></li>
< ?php endforeach; ?>
</ul>

But now consider the following code:

<ul>
< ?php echo $block->renderChildren(); ?>
</ul>

With the following block class code:

/**
 * @return string
 */
public function renderChildren()
{
    $output = '';

    foreach ($this->getFilteredCollection() as $product) {
        $block = $this->getLayout()
            ->createBlock(\Vendor\Module\Block\Example\Item::class)
            ->setTemplate('example/item.phtml')
            ->setData('product', $product);
        $output .= $block->toHtml();
    }

    return $output;
}

We now have separated our template in 2 parts:

  1. The outer part, that renders the initial template.
  2. The inner part, that renders a single item.

This way of templating brings a lot of benefits:

  • Smaller templates
  • Better separation of domain logic (block classes)
  • Easier to override / extend / modify (you don’t have to override a complete template, but only a smaller one with a single task).

The only argument you could have is that you are creating a multitude of classes and templates where it might not seem like it adds a lot of value at the moment. Personally I don’t mind having multiple files and separating the responsibilities. However, regardless of personal taste, I would strongly suggest to do this when you’re building an extension that’s likely to get extended and/or modified by 3rd party developers or modules.

Frontend Development vs. Backend Development

So basically, we now have a very simple template like this:

< ?php /** @var \Vendor\Module\Block\Example $block */ ?>
<div> ... some html ... </div>
< ?php if ($block->isEnabled()): ?>
    <ul>
    < ?php echo $block->renderChildren(); ?>
    </ul>
< ?php endif; ?>
<div> ... some html ... </div>

One could argue now: “Great! But now all of our frontend developers have to wait for the backend developers to be done!”. But you couldn’t be further away from the truth. In fact:
Frontend Developer can work much faster by working like this. Because they now only have to know a few very basic concepts of Magento 2 to create skeleton code.

“What’s that?” you might ask? Now, if we look at our template, we are calling methods on a class. So it would make perfect sense for a frontend developer to create a simple stub like this:

< ?php

namespace Vendor\Module\Block;

class Example extends \Magento\Framework\View\Element\Template
{
    /**
     * @return bool
     */
    public function isEnable()
    {
        return true;
    }

    /**
     * @return string
     */
    public function renderChildren()
    {
        $output = '';

        for ($i=0; $i&lt;10; $i+=1) {
            $block = $this->getLayout()
                ->createBlock(\Vendor\Module\Block\Example\Item::class)
                ->setTemplate('example/item.phtml');
            $output .= $block->toHtml();
        }

        return $output;
    }
}

The same ‘trick’ is of course used by the frontend developer to create a stub of the individual Item (That’s why we’re not concerned about $product at this point).
So just let the frontend developers create stub classes with @todo-statements and the backend developers will pick them up when they’re up for it. I’ve worked in teams that do it like this and in my experience this works perfectly.

The Benefits

When you start writing your templates (and block classes) like this, it won’t take long for you to start seeing benefits. These benefits include (but again, are not limited to):

  • Have isolated domain logic that is in the beginning easy to stub for frontend developers.
  • Have isolated domain logic this is in the end easy to test for automated deployment. This is great because this more or less means that you can easily unit test your templates.
  • Have templates that are much easier to modify and/or extend.
  • Have templates that are easier to understand.
  • Have domain logic can be easily reused in other parts of the application.
  • Template and business logic are not hard coupled.

But wait! What about existing templates?

This is a great question! And a very valid one as well! Up until now we have been working in an utopia. We created our own template, accompanied with our own block class. In a real world scenario we are more likely to override an existing template with an existing block class. How can we add our own domain logic to those templates without hard-coding them in the templates themselves?
There are various ways on how to tackle this. Let’s show a few of these methods. Of course there are so many different scenarios possible that there are more solutions available than these, but it’s a start

Use a Helper

You can use a helper and put the domain logic in there:

$someHelper = $this->helper('Vendor\Module\Helper\Data');

But do know that helpers (are considered an anti-pattern)[https://blogs.msdn.microsoft.com/nickmalik/2005/09/06/are-helper-classes-evil/]: Most of the time a helper class is created because you have a method that… well… won’t fit in any other given class. In my opinion helpers in Magento 2 are more of a leftover of Magento 1 and are slowly being replaced with managers and other service contracts that handle those teeny weeny tasks some helpers do. Also, you’re adding
(hard-coupling) a dependency to your template, and you’re template shouldn’t make those kind of decisions.
I tend to avoid the usage of helpers as much as possible. Most of the time it makes no sense to create helpers.

Rewrite the block class

This one makes even less sense. Rewrites are still possible in Magento 2, but since the dawn of plugins and dependency injection they are rarely to be used. Rewritten blocks can no longer be rewritten by other modules, so no: don’t do it. I’m not even going to give you an example on how to do this.

Use Dependency Injection

You can create a new class (or Service Contract) and add it to the $data-array using di.xml:

<type name=”Vendor\Module\Block\Example”>
    <arguments>
        <argument name=”data” xsi:type=”array”>
            <item name=”my-custom-stuff” xsi:type=”object”>Vendor\AnotherModule\Block\Example</item>
        </argument>
    </arguments>
</type>

The above code adds a new instance of Vendor\AnotherModule\Block\Example to your
(overwritten) template file. In that file you can access your block class like so:

/** @var \Vendor\Module\Block\Example $block */
/** @var \Vendor\AnotherModule\Block\Example $myCustomStuff */
$myCustomStuff = $block->getData(‘my-custom-stuff’);

In Conclusion

Trust me, I’ve seen templates that started small and ended up like spaghetti nightmares that even the most experienced PHP developers couldn’t make much sense of. Just let your template only worry about true or false. It doesn’t need to know how it’s determined. It’s not the templates’ responsibility.
Also, this approach makes it great to write unit tests for your block classes, reducing the chances of errors and future bugs in your templates.
So go out there and start being a responsible template developer.

The post Responsible Templating in Magento 2 appeared first on Giel Berkers dot com.


Viewing all articles
Browse latest Browse all 41

Latest Images

Trending Articles





Latest Images