Magento 2 (Adobe Commerce nella sua variante enterprise) è una delle piattaforme e‑commerce più potenti nel panorama PHP. Dal punto di vista backend, lo sviluppo si basa su concetti ben definiti: modularità, dependency injection, event-driven design, service contracts, API (REST/GraphQL), caching aggressivo e un sistema di persistenza dati che combina tabelle “flat” ed entità EAV (Entity‑Attribute‑Value) per prodotti e categorie. In questo articolo vediamo come lavorare in modo solido e “Magento‑friendly”, con esempi di codice e scelte architetturali che reggono in produzione.
1) Architettura di Magento 2 in breve
Magento 2 è costruito su:
- Moduli: unità di estensione e organizzazione del codice.
- Dependency Injection (DI): oggetti costruiti dal container tramite
di.xml. - Intercettazione via plugin (before/after/around) e event observers.
- Service Contracts: interfacce (API interne) che stabilizzano l’integrazione.
- Area / Scope: frontend, adminhtml, cron, webapi_rest, webapi_soap, ecc.
- Setup e declarative schema: creazione e migrazione DB attraverso codice.
Una regola pratica: se vuoi creare integrazioni robuste, appoggiati ai service contracts e ai repository invece di usare direttamente resource model e query SQL, salvo casi ben motivati.
2) Struttura di un modulo: registrazione e configurazione
Un modulo Magento 2 vive tipicamente in app/code/Vendor/Module (o in un pacchetto Composer). I primi due file essenziali sono:
<?php
// app/code/Vendor/Module/registration.php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Vendor_Module',
__DIR__
);
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/module.xml -->
<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">
<sequence>
<module name="Magento_Catalog"/>
</sequence>
</module>
</config>
Dopo aver creato un modulo, i comandi tipici sono:
bin/magento module:enable Vendor_Module
bin/magento setup:upgrade
bin/magento cache:flush
3) Dependency Injection: perché è centrale
Magento scoraggia l’uso di ObjectManager nel codice applicativo: la dipendenza va dichiarata e iniettata. Un esempio classico è un servizio che legge impostazioni e collabora con un repository.
<?php
declare(strict_types=1);
namespace Vendor\Module\Model;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;
use Vendor\Module\Api\FooRepositoryInterface;
final class FooService
{
private const XML_PATH_ENABLED = 'vendor_module/general/enabled';
public function __construct(
private ScopeConfigInterface $scopeConfig,
private FooRepositoryInterface $fooRepository
) {}
public function isEnabled(?int $storeId = null): bool
{
return $this->scopeConfig->isSetFlag(
self::XML_PATH_ENABLED,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
public function loadById(int $id)
{
return $this->fooRepository->getById($id);
}
}
Magento costruirà l’oggetto risolvendo le dipendenze nel container. Le preferenze e i “virtual types” sono dichiarati in etc/di.xml.
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Vendor\Module\Api\FooRepositoryInterface"
type="Vendor\Module\Model\FooRepository"/>
</config>
4) Service Contracts: interfacce e stabilità
I service contracts sono composti tipicamente da:
- Data Interface (DTO): descrive i dati trasferiti (getter/setter).
- Repository Interface: espone operazioni CRUD e ricerca.
- SearchResults: pattern per liste paginate e filtrate.
Esempio minimale di repository interface:
<?php
declare(strict_types=1);
namespace Vendor\Module\Api;
use Vendor\Module\Api\Data\FooInterface;
interface FooRepositoryInterface
{
public function getById(int $id): FooInterface;
public function save(FooInterface $foo): FooInterface;
public function delete(FooInterface $foo): bool;
public function deleteById(int $id): bool;
}
In implementazione, spesso si usa una combinazione di ResourceModel (persistenza), Factory (istanze) e CollectionFactory (liste). L’interfaccia rende l’integrazione più “future proof” e, cosa cruciale, consente a Web API di esporre metodi in modo consistente.
5) Plugin vs Observer: quando usare cosa
Plugin (interceptor)
I plugin intercettano metodi pubblici di classi concrete (non final) e consentono di:
- modificare argomenti (before)
- modificare il risultato (after)
- avvolgere l’esecuzione e decidere se chiamare l’originale (around)
Esempio: aggiungere logica dopo il salvataggio di un prodotto.
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/di.xml -->
<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_after"
type="Vendor\Module\Plugin\ProductRepositoryPlugin"
sortOrder="10"/>
</type>
</config>
<?php
declare(strict_types=1);
namespace Vendor\Module\Plugin;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Psr\Log\LoggerInterface;
final class ProductRepositoryPlugin
{
public function __construct(private LoggerInterface $logger) {}
public function afterSave(
ProductRepositoryInterface $subject,
ProductInterface $result
): ProductInterface {
$this->logger->info('Prodotto salvato: ' . $result->getSku());
return $result;
}
}
Observer (event-driven)
Gli observer ascoltano eventi e reagiscono. Sono utili quando:
- vuoi “appenderti” a un punto del flusso senza legarti a un metodo specifico
- esiste già un evento significativo (checkout, ordine, customer login, ecc.)
- vuoi un’integrazione più dichiarativa
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/events.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="sales_order_place_after">
<observer name="vendor_module_order_place_after"
instance="Vendor\Module\Observer\OrderPlaceAfter"/>
</event>
</config>
<?php
declare(strict_types=1);
namespace Vendor\Module\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Psr\Log\LoggerInterface;
final class OrderPlaceAfter implements ObserverInterface
{
public function __construct(private LoggerInterface $logger) {}
public function execute(Observer $observer): void
{
$order = $observer->getEvent()->getOrder();
$this->logger->info('Ordine piazzato: ' . $order->getIncrementId());
}
}
Scelta pratica: usa plugin per cambiare comportamento puntuale (soprattutto su service contracts), usa observer per azioni reattive e orchestrazione.
6) Persistenza: EAV, tabelle flat e ResourceModel
Magento usa EAV per alcune entità molto flessibili (prodotti, categorie, clienti in parte) e tabelle “normali” per molte altre (ordini, quote, ecc.). A livello backend, questo implica:
- query su EAV potenzialmente costose: attenzione a filtri e join
- necessità di indicizzazione per prestazioni accettabili
- preferire repository/collection rispetto a query manuali, salvo ottimizzazioni mirate
Per entità custom, oggi è consigliato usare Declarative Schema (Magento 2.3+). Esempio di tabella custom:
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/db_schema.xml -->
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="vendor_module_foo" resource="default" engine="innodb" comment="Foo entity">
<column xsi:type="int" name="entity_id" unsigned="true" nullable="false" identity="true" comment="Entity ID"/>
<column xsi:type="varchar" name="code" nullable="false" length="64" comment="Code"/>
<column xsi:type="timestamp" name="created_at" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/>
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="entity_id"/>
</constraint>
<index referenceId="VENDOR_MODULE_FOO_CODE" indexType="btree">
<column name="code"/>
</index>
</table>
</schema>
Quando serve aggiungere dati in fase di deploy, usa Data Patch (setup patch) invece di vecchi script di install/upgrade.
7) Web API: REST e GraphQL
Magento espone API REST e GraphQL. Dal punto di vista backend, ciò impatta in due modi:
- definisci contratti chiari (service contracts), perché sono la base per esporre servizi
- pensa a validazione, autorizzazioni (ACL) e performance (payload piccoli, query efficienti)
Esempio di esposizione REST via webapi.xml:
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/webapi.xml -->
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<route url="/V1/vendor/foo/:id" method="GET">
<service class="Vendor\Module\Api\FooRepositoryInterface" method="getById"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
</routes>
Per GraphQL, la logica vive nei resolver e nella definizione di schema. È importante minimizzare N+1 query: usa data loader, repository ottimizzati o query aggregate quando necessario.
8) Adminhtml: ACL, menu, UI component e controller
Nel backend amministrativo, si lavora con:
- ACL per permessi granulari
- menu.xml per voce di menu
- controller per azioni (list, edit, save, delete)
- UI component per griglie e form
Esempio ACL:
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/acl.xml -->
<acl xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
<resources>
<resource id="Magento_Backend::admin">
<resource id="Vendor_Module::root" title="Vendor Module" sortOrder="10">
<resource id="Vendor_Module::foo" title="Gestione Foo" sortOrder="10"/>
</resource>
</resource>
</resources>
</acl>
Nel controller di salvataggio, il punto chiave è gestire validazione, transazioni e messaggi in modo coerente con la UX di admin.
9) Prestazioni: cache, indexer, cron e code
Magento è estremamente sensibile alle performance. Le leve principali lato backend sono:
- Cache (config, layout, block_html, full page cache). Non “bucare” la cache senza motivo.
- Indexer: molti dati derivati vengono precomputati. Se tocchi attributi o entità chiave, valuta l’impatto sull’indicizzazione.
- Cron: attività asincrone e batch. Evita di fare lavori lunghi in request/response.
- Message Queue: per pipeline asincrone (a seconda della versione e configurazione).
Esempio di cron job:
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/crontab.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd">
<group id="default">
<job name="vendor_module_cleanup" instance="Vendor\Module\Cron\Cleanup" method="execute">
<schedule>0 3 * * *</schedule>
</job>
</group>
</config>
<?php
declare(strict_types=1);
namespace Vendor\Module\Cron;
use Psr\Log\LoggerInterface;
final class Cleanup
{
public function __construct(private LoggerInterface $logger) {}
public function execute(): void
{
// Eseguire pulizie batch, preferibilmente a chunk e con checkpoint.
$this->logger->info('Cleanup completato');
}
}
Consiglio pragmatico: se un processo può crescere in durata o carico, portalo su cron/queue e rendilo idempotente.
10) Qualità del codice: test, static analysis e standard
In Magento, la qualità del backend si mantiene con:
- Unit test (PHPUnit) per logica pura e servizi
- Integration test per repository, setup, observer e flussi complessi
- Coding standard (Magento/PSR) e strumenti come PHP_CodeSniffer
- Static analysis (PHPStan/Psalm) adattata al progetto
Esempio di unit test (semplificato) su un servizio:
<?php
declare(strict_types=1);
namespace Vendor\Module\Test\Unit\Model;
use Magento\Framework\App\Config\ScopeConfigInterface;
use PHPUnit\Framework\TestCase;
use Vendor\Module\Api\FooRepositoryInterface;
use Vendor\Module\Model\FooService;
final class FooServiceTest extends TestCase
{
public function testIsEnabledReturnsBool(): void
{
$scopeConfig = $this->createMock(ScopeConfigInterface::class);
$repo = $this->createMock(FooRepositoryInterface::class);
$scopeConfig->method('isSetFlag')->willReturn(true);
$service = new FooService($scopeConfig, $repo);
self::assertTrue($service->isEnabled());
}
}
11) Sicurezza: input, ACL, secrets e hardening
Per uno sviluppo backend serio su Magento:
- valida e sanitizza input, soprattutto in admin e Web API
- usa ACL corretti: evitare
anonymousse non strettamente necessario - non salvare segreti nel repository (chiavi API, token), preferisci variabili d’ambiente e vault
- rispetta il modello di autorizzazione e le policy di sessione
- mantieni la piattaforma aggiornata e monitora le patch di sicurezza
12) Workflow di sviluppo: environment, deploy e debugging
Un workflow tipico include:
- Ambiente locale con Docker o VM, configurato con Elasticsearch/OpenSearch, Redis e un DB dedicato.
- Developer mode per sviluppare, production mode per testare performance realistiche.
- Compilazione DI, static content deploy e gestione cache in pipeline CI/CD.
- Debug con Xdebug e log strutturati (Monolog/PSR-3).
Comandi utili in sviluppo:
bin/magento deploy:mode:set developer
bin/magento cache:clean
bin/magento indexer:reindex
bin/magento setup:di:compile
Checklist finale per un backend Magento “a prova di produzione”
- Usa service contracts e repository per le integrazioni.
- Evita
ObjectManagernel codice: DI e costruttori tipizzati. - Preferisci patch (schema/data) e declarative schema per DB.
- Gestisci performance: cache, indexer, cron/queue, query efficienti.
- Scrivi test per servizi critici e flussi di business.
- Applica ACL corretti, valida input e gestisci segreti fuori dal codice.
Con questi principi, lo sviluppo backend in PHP su Magento diventa molto più prevedibile: i moduli rimangono manutenibili, le integrazioni resistono agli upgrade e le prestazioni restano controllabili anche in contesti con cataloghi grandi e traffico elevato.