Sviluppo backend in PHP con Magento

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 anonymous se 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:

  1. Ambiente locale con Docker o VM, configurato con Elasticsearch/OpenSearch, Redis e un DB dedicato.
  2. Developer mode per sviluppare, production mode per testare performance realistiche.
  3. Compilazione DI, static content deploy e gestione cache in pipeline CI/CD.
  4. 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 ObjectManager nel 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.

Torna su