Magento 2

Magento fundamentals: helper decomposition

5 minutes reading

What’s an helper

According to Wikipedia’s definition, “in object-oriented programming, a helper class is used to assist in providing some functionality, which isn’t the main goal of the application or class in which it is used.”

Helpers used to be first-class citizens in Magento 1, which left many tracks into Magento 2 codebase.

That’s the reason why, I guess, a lot of Magento developers kept on implementing their helpers, violating the new Magento 2 guidelines.

Why helpers don’t respect the new Magento 2 guidelines

Since, by definition, a helper is a class with many responsibilities, it doesn’t adhere to the class design principles defined in the Magento technical guidelines.

To be more specific, according to the single responsibility principle, “there should never be more than one reason for a class to change”.

In Magento 2, many helpers extend the \Magento\Framework\App\Helper\AbstractHelper base class.

In Magento 2.4.2, this class alone depends on:

  • \Magento\Framework\Module\Manager
  • \Psr\Log\LoggerInterface
  • \Magento\Framework\App\RequestInterface
  • \Magento\Framework\UrlInterface
  • \Magento\Framework\HTTP\Header
  • \Magento\Framework\Event\ManagerInterface
  • \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress
  • \Magento\Framework\Cache\ConfigInterface
  • \Magento\Framework\Url\EncoderInterface
  • \Magento\Framework\Url\DecoderInterface
  • \Magento\Framework\App\Config\ScopeConfigInterface

This class is a perfect example of a Swiss Army knife with more than one reason to change.

What does it mean to decompose a helper?

Let’s start with the fact that helpers don’t provide data (more on this later) but behaviors.

In Magento, behaviors should be implemented as service classes, adhering as much as possible to the following principles:

  • they don’t have a mutable state;
  • they are usually instantiated as singletons, using constructor-based dependency injection;
  • they have a name that reveals their intent, generally starting with a verb at the imperative form;
  • they have a single public execute() method which fulfills the purpose.

💡 Note that there are many service classes in Magento that don’t follow all the above rules: repositories, resource models, and many others. The most important of the rules that we should try to apply is related to the immutable state, that protects from unpredictable side effects. The adherence to the others is not always mandatory, as we will see in an example below.

If we look at our helpers, we can see that, after all, they are nothing but a set of behaviors packed together in a single class.

Decomposing them won’t be a difficult task.

But helpers provide data!

When we read that a service class doesn’t provide data, it doesn’t mean that it can’t provide data taken from a persistence layer.

It means that a service class is not an entity; it doesn’t even have an identity and a state and doesn’t persist and retrieve its data.

It can provide other entities’ data. For example, a repository is a service class that provides data access (a.k.a. CRUD) methods for a given entity.

How to decompose a helper

At this point, we are ready to decompose our helpers into different service classes.

It’s just a matter of identifying the different behaviors or reasons to change our helper class and split them into several service classes.

💡 When doing this, we are recommended to follow the Service Contracts guidelines.

An example is worth more than a thousand words.

Let’s say we have the following helper class (methods’ implementation details don’t matter at this point):

<?php declare(strict_types=1);

namespace MyCoolVendor\MyCoolModule\Helper;

class Data extends \Magento\Framework\Url\Helper\Data 
{
    public function isEnabled(): bool
    {
        // use $this->scopeConfig->isSetFlag() 
        // to retrieve the configuration value
    }

    public function getApiKey(): string
    {
        // use $this->scopeConfig->getValue() 
        // to retrieve the configuration value
    }

    public function getActionUrl(int $customerId): string
    {
        // use $this->_urlBuilder->getUrl() 
        // to build an action URL given a customer ID
    }
}

The above class exposes two main behaviors:

  • give access to some configuration values;
  • build an action URL given a customer id.

Thus, we will define two service contracts and their corresponding implementation:

  • a service class that gives access to the configuration values; this class won’t have a single execute() method but a method for each configuration we want to retrieve.
    This is an acceptable compromise that allows us to avoid defining a service class for each configuration value, which would be overwhelming;
  • a service class that allows us to get an action URL given a specific customer; maybe the logic may differ depending on the fact the customer is a guest or a registered user; we encapsulate this behavior inside the class implementation and represents the only reason for future changes.

The resulting code structure is shown below.

Service contracts declarations in MyCoolVendor_MyCoolModuleApi

<?php declare(strict_types=1);

namespace MyCoolVendor\MyCoolModuleApi\Api;

interface ConfigInterface
{
    public function isEnabled(): bool;

    public function getApiKey(): string;
}
<?php declare(strict_types=1);

namespace MyCoolVendor\MyCoolModuleApi\Api;

interface GetActionUrlByCustomerInterface
{
    public function execute(): string;
}

Service contracts implementations in MyCoolVendor_MyCoolModule

<?php declare(strict_types=1);

namespace MyCoolVendor\MyCoolModule\Model;

use \Magento\Framework\App\Config\ScopeConfigInterface;
use \MyCoolVendor\MyCoolModuleApi\Api\ConfigInterface;

class Config implements ConfigInterface
{
    private ScopeConfigInterface $scopeConfig;

    public function __construct(ScopeConfigInterface $scopeConfig)
    {
        $this->scopeConfig = $scopeConfig;
    }

    public function isEnabled(): bool
    {
        // use $this->scopeConfig->isSetFlag() 
        // to retrieve the configuration value
    }

    public function getApiKey(): string
    {
        // use $this->scopeConfig->getValue() 
        // to retrieve the configuration value
    }
}
<?php declare(strict_types=1);

namespace MyCoolVendor\MyCoolModule\Model;

use \Magento\Framework\UrlInterface;
use \MyCoolVendor\MyCoolModuleApi\Api\GetActionUrlByCustomerInterface;

class GetActionUrlByCustomer implements GetActionUrlByCustomerInterface
{
    private UrlInterface $urlBuilder;

    public function __construct(UrlInterface $urlBuilder)
    {
        $this->urlBuilder = $urlBuilder;
    }

    public function execute(int $customerId): string
    {
        // use $this->urlBuilder->getUrl() 
        // to build an action URL given a customer ID
    }
}

Dependency injection bindings

<?xml version="1.0"?>
<!-- file: app/code/MyCoolVendor/MyCoolModule/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="MyCoolVendor\MyCoolModuleApi\Api\ConfigInterface" 
        type="MyCoolVendor\MyCoolModule\Model\Config"/>
    <preference for="\MyCoolVendor\MyCoolModuleApi\Api\GetActionUrlByCustomerInterface" 
        type="MyCoolVendor\MyCoolModule\Model\GetActionUrlByCustomer"/>
</config>

Conclusion

Adhering to others’ coding guidelines may be tedious because it forces us to abandon our habits and our comfort zone.

But as soon as we realize the benefits, it pays back. Everyone’s code follows the same structure and rules, becomes more readable, improving stability and maintainability in the long term.

Give it a try!

Post of

COO | Reggio Emilia

Alessandro works at Bitbull as an experienced technical leader devoted to software design, development, and mentoring.
Honored three times with the title of Magento Master and listed among the top 50 contributors in the last years, he is also an active Magento Community Maintainer since 2018 and member of the Magento Association content committee since 2020.