In Magento 2, un plugin (detto anche interceptor) è un meccanismo che permette di eseguire logica prima, dopo o intorno a un metodo pubblico di una classe (o di un’interfaccia) senza dover riscrivere la classe originale. È uno degli strumenti più utili per personalizzare il comportamento del core o di moduli di terze parti in modo aggiornabile e con impatto ridotto.
Quando usare un plugin (e quando no)
- Usa un plugin se devi intercettare un metodo pubblico e aggiungere/alterare input e output con minima invasività.
- Preferisci un observer se esiste già un evento adatto e vuoi reagire a un’azione senza toccare un metodo specifico.
- Evita i plugin su classi con metodi protetti/privati (non intercettabili) o dove l’ordine di esecuzione tra più plugin può diventare fragile.
- Evita i plugin su metodi ad altissima frequenza se l’overhead è significativo (misura prima e ottimizza dopo).
Requisiti e struttura di base di un modulo
Un plugin vive sempre dentro un modulo Magento. Se non hai già un modulo, creane uno con la classica struttura:
app/code/Vendor/Module/
├─ registration.php
├─ etc/module.xml
└─ etc/di.xml
registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Vendor_Module',
__DIR__
);
etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Vendor_Module" setup_version="1.0.0"/>
</config>
Una volta creato il modulo, abilitalo e aggiorna lo schema dei moduli:
bin/magento module:enable Vendor_Module
bin/magento setup:upgrade
Come funzionano i plugin in Magento 2
Magento genera classi “interceptor” in generated/ (o var/generation nelle versioni più vecchie),
che estendono la classe target e “avvolgono” le chiamate ai suoi metodi pubblici.
La configurazione dei plugin avviene tramite Dependency Injection in etc/di.xml.
Esistono tre tipologie principali di plugin:
- before: esegue logica prima del metodo e può modificare gli argomenti.
- after: esegue logica dopo il metodo e può modificare il risultato.
- around: avvolge l’esecuzione del metodo e decide se e quando chiamare il metodo originale tramite
$proceed.
Esempio completo: aggiungere un attributo al nome prodotto al salvataggio
In questo esempio intercettiamo il salvataggio del prodotto per aggiungere un suffisso al nome (dimostrativo). L’obiettivo è capire struttura e flusso, non implementare una regola business definitiva.
1) Scegliere il metodo target
Una scelta comune è intercettare un servizio di dominio invece di una classe “modello”.
Qui usiamo l’interfaccia Magento\Catalog\Api\ProductRepositoryInterface e il metodo save.
2) Configurare il plugin in etc/di.xml
Dichiariamo il plugin associandolo al tipo (interfaccia o classe) che vogliamo intercettare.
L’attributo sortOrder gestisce l’ordine relativo tra plugin sullo stesso metodo.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Catalog\Api\ProductRepositoryInterface">
<plugin name="vendor_module_product_save_plugin"
type="Vendor\Module\Plugin\ProductSavePlugin"
sortOrder="10"/>
</type>
</config>
3) Creare la classe plugin
La classe plugin implementa metodi con naming convenzionale:
beforeSave, afterSave, aroundSave,
dove “Save” corrisponde al nome del metodo target. La prima variabile è sempre $subject
(l’istanza della classe intercettata) e, nel caso di around, il secondo parametro è callable $proceed.
Creiamo Vendor/Module/Plugin/ProductSavePlugin.php:
<?php
declare(strict_types=1);
namespace Vendor\Module\Plugin;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
class ProductSavePlugin
{
/**
* before plugin: può modificare gli argomenti (ritornando un array di argomenti).
*
* Firma target: save(ProductInterface $product, $saveOptions = false)
*/
public function beforeSave(
ProductRepositoryInterface $subject,
ProductInterface $product,
$saveOptions = false
): array {
$name = (string) $product->getName();
// Esempio dimostrativo: aggiunge un suffisso se non presente.
$suffix = ' - Custom';
if ($name !== '' && mb_substr($name, -mb_strlen($suffix)) !== $suffix) {
$product->setName($name . $suffix);
}
return [$product, $saveOptions];
}
/**
* after plugin: può modificare il risultato.
*/
public function afterSave(
ProductRepositoryInterface $subject,
ProductInterface $resultProduct,
ProductInterface $originalProduct,
$saveOptions = false
): ProductInterface {
// Qui potresti, ad esempio, arricchire il prodotto di ritorno o fare logging.
return $resultProduct;
}
}
Nota sulle firme: in un after il primo parametro dopo $subject è sempre il
risultato del metodo originale. Successivamente Magento passa gli argomenti originali del metodo.
Plugin around: controllo totale (da usare con cautela)
Un plugin around permette di:
- modificare gli argomenti prima della chiamata;
- decidere se chiamare il metodo originale;
- modificare il risultato e gestire eccezioni;
- misurare tempi o aggiungere transazioni/logica di fallback.
È potente ma aumenta il rischio di conflitti e side effect, specialmente se più moduli definiscono around sullo stesso metodo. In generale: preferisci before/after se bastano.
<?php
declare(strict_types=1);
namespace Vendor\Module\Plugin;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
class ProductSaveAroundPlugin
{
public function aroundSave(
ProductRepositoryInterface $subject,
callable $proceed,
ProductInterface $product,
$saveOptions = false
): ProductInterface {
// Pre-logica: ad esempio validazioni custom
if ((string) $product->getName() === '') {
// Potresti lanciare un’eccezione o impostare un valore di default
$product->setName('Default name');
}
// Chiamata al metodo originale
$result = $proceed($product, $saveOptions);
// Post-logica: modifica risultato o ulteriori azioni
return $result;
}
}
Scope di configurazione: global, frontend, adminhtml, webapi_rest
Un plugin può essere dichiarato in diversi file di.xml a seconda del contesto:
etc/di.xml: globale (default)etc/frontend/di.xml: solo area frontendetc/adminhtml/di.xml: solo backofficeetc/webapi_rest/di.xmleetc/webapi_soap/di.xml: solo API
Usare lo scope corretto aiuta performance e riduce impatti indesiderati. Ad esempio, un plugin necessario solo in admin non dovrebbe attivarsi anche in frontend.
Ordine di esecuzione e conflitti tra plugin
Se più plugin intercettano lo stesso metodo, Magento li esegue in base a:
sortOrder: numero intero; più basso significa eseguito prima (relativamente agli altri).disabled: puoi disabilitare un plugin da un altro modulo (con attenzione e motivazione chiara).
Regole pratiche:
- mantieni
sortOrdersolo quando serve davvero; - se un plugin dipende da un altro, documentalo chiaramente;
- evita catene di around su metodi critici.
Disabilitare un plugin (caso avanzato)
Può capitare di dover disabilitare un plugin di terze parti per sostituirlo con un’implementazione diversa.
Si può fare in di.xml dichiarando un plugin con lo stesso nome e disabled="true".
È una pratica da usare con cautela perché può cambiare il comportamento atteso da quel modulo.
<type name="Magento\Catalog\Api\ProductRepositoryInterface">
<plugin name="thirdparty_plugin_name" disabled="true"/>
</type>
Compilazione, cache e generazione classi
In modalità produzione o quando lavori con DI compilata, dopo aver aggiunto o modificato plugin è spesso necessario:
bin/magento cache:flush
bin/magento setup:di:compile
In sviluppo, valuta anche di pulire generated/ se hai cambiato firme o classi in modo significativo
(attenzione a farlo in modo compatibile con la tua modalità di deploy).
Debug: capire se il plugin viene eseguito
Tecniche utili:
- Log dedicati tramite
Psr\Log\LoggerInterface(meglio divar_dumpe simili). - Breakpoint con Xdebug nel metodo del plugin.
- Verifica del wiring DI: controlla che il
typeindi.xmlsia corretto (interfaccia vs classe concreta). - Ricorda che i plugin intercettano solo metodi pubblici e solo quando l’istanza è risolta dal DI container (non tramite
new).
Esempio di logging nel plugin
<?php
declare(strict_types=1);
namespace Vendor\Module\Plugin;
use Psr\Log\LoggerInterface;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
class ProductSavePluginWithLogger
{
public function __construct(
private readonly LoggerInterface $logger
) {}
public function beforeSave(
ProductRepositoryInterface $subject,
ProductInterface $product,
$saveOptions = false
): array {
$this->logger->info('Product save intercepted', [
'sku' => $product->getSku(),
'name' => $product->getName()
]);
return [$product, $saveOptions];
}
}
Best practice per plugin robusti
- Intercetta servizi, non modelli: meglio repository e service contracts rispetto a classi “interne”.
- Minimizza l’alterazione: fai solo ciò che serve, senza introdurre side effect non necessari.
- Gestisci eccezioni con criterio: se usi around, non nascondere errori importanti.
- Non duplicare logica core: preferisci composizione e piccole modifiche.
- Documenta il perché: lascia commenti e README quando un plugin modifica flussi critici (checkout, prezzi, stock).
- Test: almeno test di integrazione sui metodi che intercetti, specialmente su aggiornamenti Magento.
Problemi comuni
- Il plugin non scatta: metodo non pubblico, classe istanziata con
new, di.xml nello scope sbagliato, cache/DI non compilata. - Firma errata: parametri non allineati al metodo originale; negli after l’ordine è (result, args originali).
- Conflitti con altri moduli: più plugin sullo stesso metodo; cerca sortOrder e riduci around.
- Loop o chiamate ricorsive: se dentro il plugin richiami lo stesso metodo intercettato, rischi un loop (usa servizi alternativi o condizioni di guardia).
Checklist finale
- Modulo registrato e abilitato.
di.xmlnello scope corretto e target giusto (classe/interfaccia).- Classe plugin con namespace e percorso file corretti.
- Firme
before/after/aroundcoerenti col metodo target. - Cache flush e, se necessario,
setup:di:compile. - Test manuali e/o automatizzati sul flusso intercettato.
Con questi elementi puoi costruire plugin puliti e mantenibili, limitando override e riducendo i rischi durante gli aggiornamenti. Il passo successivo è scegliere punti di estensione più “stabili” (service contracts) e coprire i casi critici con test di integrazione.