Magento fundamentals: decomporre un helper
Lettura 6 minutiCos’è un helper
Stando alla definizione su Wikipedia, “nella programmazione orientata agli oggetti, una classe helper è utilizzata per fornire funzioni utili che non rappresentano lo scopo principale dell’applicazione in cui sono utilizzate.”
Gli helper avevano un ruolo primario in Magento 1 e questo ha lasciato molte tracce anche nel codice di Magento 2, dove sono presenti numerose classi helper.
A mio parere, questo è il motivo per cui molti sviluppatori hanno continuato a implementare le proprie classi helper in Magento, violandone però le linee guida.
Perché gli helper non rispettano le linee guida di Magento 2?
Per definizione, una classe helper fornisce funzioni con diversa utilità e quindi ha molte responsabilità, il che viola i principi di sviluppo di classi definiti nelle linee guida tecniche di Magento.
Nello specifico, un helper viola il cosiddetto single responsibility principle, secondo cui una classe non dovrebbe avere più di un motivo per apportare una modifica.
Molti degli helper presenti in Magento 2estendono la classe \Magento\Framework\App\Helper\AbstractHelper
.
In Magento 2.4.2, tale classe dipende dalle seguenti:
\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
Ecco quindi un perfetto esempio di coltellino svizzero che ha ben più di un motivo per apportare modifiche.
Cosa significa “decomporre un helper”?
Partiamo dal presupposto che lo scopo di un helper non è fornire dati (in seguito approfondisco questo aspetto) ma comportamenti, anche detti behavior.
Stando alle linee guida, i comportamenti si implementano con classi di servizio (service class) che aderiscano il più possibile ai seguenti principi:
- non hanno uno stato mutabile;
- sono istanziate generalmente come singleton (istanze condivise) dal sistema di dependency injection dichiarandone la dipendenza attraverso i parametri del costruttore;
- hanno nomi che ne rivelano lo scopo; generalmente si usano verbi all’imperativo;
- hanno un unico metodo pubblico chiamato
execute()
che attua il comportamento atteso.
💡 Da notare che in Magento sono presenti molte classi di servizio che non seguono del tutto i principi sopra elencati, come i repository, i resource model e molte altre. La regola più importante a cui dovremmo cercare di attenerci è quella relativa allo stato immutabile che ci mette al riparo dall’avere effetti collaterali indesiderati. Gli altri principi sono auspicabili ma non obbligatori, come vedremo nell’esempio riportato in seguito.
Se guardiamo agli helper come li abbiamo implementati sino ad oggi, ci renderemo conto che alla fin fine sono solo un insieme di comportamenti impacchettati in una sola classe.
Decomporli a questo punto non sarà difficile.
Ma non è vero che gli helper non forniscono dati!
Come promesso, eccomi a trattare questo punto. Leggere che una classe di servizio non fornisce dati non va interpretato alla lettera: tra gli scopi di questo genere di classi c’è proprio quello di fornire astrazioni sul livello di persistenza.
Il “non fornire dati” si riferisce al fatto che le classi di servizio non sono entità; non hanno una identità (una chiave primaria, per intenderci) né uno stato e non scrivono né recuperano dati propri.
Possono servire, come anticipato, per accedere ai dati di altre entità. Ad esempio i repository sono classi di servizio che forniscono metodi di accesso ai dati (noti anche come CRUD) di una certa entità.
Come decomporre un helper
A questo punto dovremmo essere pronti a decomporre un helper in diverse classi di servizio.
Si tratta di identificare i diversi comportamenti, o “motivi per apportare modifiche”, della nostra classe helper e smistarli in classi differenti.
💡 Quando scriviamo le nuove service class, è bene attenersi alle linee guida sui service contract.
Credo che un esempio valga più di mille parole, quindi riporto di seguito dei frammenti di codice estratti dai moduli scritto per decomporre l’helper seguente (i dettagli di implementazione sono omessi perché non aggiungono nulla di utile):
<?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
}
}
La classe helper sopra riportata espone due comportamenti principali:
- dà accesso a valori di configurazione;
- fornisce un metodo per generare un URL partendo da un customer id.
Pertanto, definiamo due service contract e le rispettive implementazioni:
- una classe di servizio che dà accesso ai valori di configurazione; questa classe non ha un unico metodo
execute()
ma un metodo per ciascun valore di configurazione da esporre.
Come anticipato, ci sono compromessi accettabili nel disegnare le nostre classi di servizio e questo è uno di quelli; serve ad evitare di dover implementare una classe di servizio per ciascun valore di confgurazione. A volte possono essere decine e questo sarebbe davvero troppo oneroso. - una classe di servizio che permette di generare un URL dato un customer id; la logica per generare l’URL potrebbe cambiare a seconda che il customer sia o meno registrato ma ora come ora non ci interessa, questo comportamento sarà incapsulato nell’implementazione e sarà l’unico motivo per cui questa potrebbe cambiare in futuro.
Il codice derivante da questa decomposizione è riportato di seguito.
Dichiarazione dei service contract 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;
}
Implementazione dei service contract 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
}
}
Binding per il funzionamento della dependency injection
<?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>
Conclusioni
Capisco che adeguarsi a linee guida dettate da altri può essere noioso, perché ci obbliga ad abbandonare le nostre abitudini e la nostra comfort zone.
Ma appena ci rendiamo conto dei benefici, comprendiamo anche perché ne vale la pena. Il codice di tutti segue la stessa struttura e aderisce alle stesse regole, diventa più leggibile, a tutto vantaggio della sua stabilità e manutenibilità a lungo termine.
Provare per credere!
Articolo scritto da
Alessandro Ronchi☝ Ti piace quello che facciamo? Unisciti a noi!