Symfony Dependency Injection: service container, autowiring e tag nel 2026

Approfondimento sulla dependency injection di Symfony: service container, autowiring, l'attributo #[Autowire], binding delle interfacce, servizi taggati con #[AutowireIterator], compiler pass e servizi lazy in Symfony 7.4 e 8.0.

Diagramma dell'architettura di dependency injection di Symfony: service container e autowiring

La dependency injection di Symfony è la spina dorsale di ogni applicazione Symfony: il service container istanzia, configura e collega gli oggetti tra loro, così che le classi dichiarino ciò di cui hanno bisogno invece di costruirlo da sé. In Symfony 7.4 e 8.0 l'autowiring, gli attributi PHP e i servizi taggati hanno ridotto la configurazione esplicita quasi a zero, mantenendo al contempo il container completamente risolto e prevedibile in fase di compilazione. Capire come funziona questa risoluzione separa gli sviluppatori che combattono contro il container da quelli che lo lasciano lavorare.

Che cos'è la dependency injection in Symfony?

La dependency injection in Symfony è un pattern in cui il service container passa le dipendenze di un oggetto attraverso il suo costruttore anziché lasciare che sia l'oggetto a crearle. L'autowiring risolve automaticamente ogni argomento tipizzato verso un servizio corrispondente, per cui la maggior parte delle classi non richiede alcuna configurazione manuale.

Come il service container di Symfony risolve le dipendenze

Il service container è una classe PHP compilata che sa come costruire ogni servizio dell'applicazione. Durante la fase di compilazione del container, Symfony analizza le definizioni delle classi, legge le firme dei costruttori e produce una factory memorizzata in cache. A runtime, la richiesta di un servizio restituisce un'istanza condivisa costruita in modo lazy. Poiché il collegamento avviene in fase di compilazione, una dipendenza mal configurata fallisce quando viene costruita la cache, non quando una richiesta raggiunge la produzione.

Un servizio è semplicemente una classe che svolge un compito ed è gestita dal container. Il file config/services.yaml predefinito registra tutto ciò che si trova sotto src/ come servizio con autowiring e autoconfiguration abilitati, ed è per questo che una classe qualunque funziona senza alcuna impostazione aggiuntiva.

yaml
# config/services.yaml
services:
    _defaults:
        autowire: true       # inject arguments by type-hint
        autoconfigure: true  # apply tags based on implemented interfaces

    App\:
        resource: '../src/'
        exclude:
            - '../src/Entity/'
            - '../src/Kernel.php'

Con questi valori predefiniti, una classe richiede i propri collaboratori attraverso il costruttore e il container li fornisce. Nessuna factory, nessun new, nessuna lista di argomenti manuale.

src/Notification/SlackNotifier.phpphp
namespace App\Notification;

use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final class SlackNotifier
{
    // The container reads each type-hint and injects a matching service
    public function __construct(
        private readonly HttpClientInterface $httpClient,
        private readonly LoggerInterface $logger,
    ) {}

    public function send(string $channel, string $message): void
    {
        $this->httpClient->request('POST', 'https://slack.example/api/post', [
            'json' => ['channel' => $channel, 'text' => $message],
        ]);
        $this->logger->info('Slack message dispatched', ['channel' => $channel]);
    }
}

Il container istanzia SlackNotifier una sola volta, inietta l'HTTP client e il logger condivisi e riutilizza quell'istanza ovunque venga richiesta. È proprio questo comportamento condiviso per impostazione predefinita a rendere il container veloce ed efficiente nella memoria. Il modello completo è documentato nella guida ufficiale al service container di Symfony, che si basa sullo standard dell'interfaccia container PSR-11 condiviso in tutto l'ecosistema PHP.

La condivisione può essere disattivata quando serve un'istanza fresca a ogni richiesta, per esempio un builder con stato che accumula dati. Impostare shared: false su una definizione la trasforma in una factory che restituisce un nuovo oggetto a ogni iniezione. Nella pratica è raro, perché la maggior parte dei servizi sono collaboratori senza stato, ma sapere che questa impostazione predefinita esiste e come sovrascriverla conta quando un bug risale a due consumatori che modificano inaspettatamente lo stesso oggetto.

Autowiring in Symfony: type hint e l'attributo Autowire

L'autowiring di Symfony risolve gli argomenti del costruttore in base alla loro dichiarazione di tipo. Quando un argomento è tipizzato con una classe o un'interfaccia che corrisponde a esattamente un servizio, il container lo inietta senza alcuna configurazione. L'autowiring si arresta solo davanti a valori che non sono servizi: scalari, variabili d'ambiente e parametri. L'attributo #[Autowire] colma questa lacuna direttamente sull'argomento, mantenendo il collegamento accanto al codice che lo utilizza.

src/Service/PaymentGateway.phpphp
namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class PaymentGateway
{
    public function __construct(
        // Inject a container parameter with the %...% syntax
        #[Autowire('%kernel.environment%')]
        private readonly string $environment,

        // Inject an environment variable, resolved at runtime
        #[Autowire(env: 'STRIPE_SECRET')]
        private readonly string $apiKey,

        // Bind a specific service when the type alone is ambiguous
        #[Autowire(service: 'monolog.logger.payment')]
        private readonly LoggerInterface $logger,
    ) {}
}

Ogni argomento dichiara la propria sorgente: un parametro, una variabile d'ambiente o un servizio con nome. L'attributo accetta anche un argomento expression: per valori dell'Expression Language, utile quando una dipendenza deriva da un calcolo a runtime anziché da una definizione statica. Tenere questi metadati sul costruttore significa che chi legge vede esattamente da dove proviene ogni valore senza aprire un file YAML separato. Il riferimento all'attributo si trova nella documentazione sull'autowiring di Symfony.

Tipizza le interfacce, non le classi concrete

Fare autowiring su una classe concreta come S3Storage accoppia ogni consumatore a quella implementazione e rende doloroso sostituirla nei test. Meglio tipizzare l'interfaccia e lasciare che #[AsAlias] o #[Autowire(service: ...)] decidano il servizio concreto. Iniettare l'intero container per recuperare i servizi a mano vanifica i controlli in fase di compilazione ed è proprio l'anti-pattern che l'autowiring esiste per eliminare.

Legare le interfacce quando esistono più implementazioni

L'autowiring contro un'interfaccia funziona in modo pulito finché due classi non la implementano. A quel punto il container non può indovinare quale iniettare e solleva un errore di ambiguità in fase di compilazione. Due attributi risolvono la situazione: #[AsAlias] indica un'implementazione come predefinita per l'interfaccia, mentre #[Autowire(service: ...)] fissa una specifica implementazione su un singolo argomento.

src/Storage/StorageInterface.phpphp
namespace App\Storage;

interface StorageInterface
{
    public function put(string $key, string $contents): void;
}
src/Storage/S3Storage.phpphp
namespace App\Storage;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

// Becomes the default service returned for StorageInterface
#[AsAlias(StorageInterface::class)]
final class S3Storage implements StorageInterface
{
    public function put(string $key, string $contents): void { /* upload to S3 */ }
}
src/Service/BackupService.phpphp
namespace App\Service;

use App\Storage\LocalStorage;
use App\Storage\StorageInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class BackupService
{
    public function __construct(
        // Resolves to the #[AsAlias] default (S3Storage)
        private readonly StorageInterface $primary,

        // Pins the concrete implementation explicitly by service id
        #[Autowire(service: LocalStorage::class)]
        private readonly StorageInterface $fallback,
    ) {}
}

L'attributo #[Target] offre una terza opzione: fa riferimento a un'implementazione tramite il suo nome di alias di autowiring, disaccoppiando il nome della variabile dell'argomento dalla risoluzione. La scelta tra questi approcci dipende dall'intento. Usare #[AsAlias] quando un'implementazione è davvero quella predefinita nell'intero codebase, e #[Autowire(service: ...)] o #[Target] quando un singolo consumatore ha bisogno di una variante non predefinita. Questo pattern ricorre di continuo nelle applicazioni in produzione ed è un tema molto amato nel modulo di colloquio sulla dependency injection di Symfony.

Pronto a superare i tuoi colloqui su Symfony?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

I tag dei servizi Symfony e il collector AutowireIterator

I tag sono il modo in cui Symfony raggruppa i servizi correlati affinché un altro servizio possa consumarli tutti in una volta. È così che prendono vita i pattern strategy, chain-of-responsibility e plugin che compaiono in tutto il framework stesso: le estensioni dei form type, le estensioni Twig e gli event subscriber sono tutti guidati dai tag. Anziché taggare manualmente, #[AutoconfigureTag] posto su un'interfaccia dice al container di taggare automaticamente ogni classe che la implementa.

src/Export/ExporterInterface.phpphp
namespace App\Export;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

// Every class implementing this interface is tagged automatically
#[AutoconfigureTag('app.exporter')]
interface ExporterInterface
{
    public function supports(string $format): bool;
    public function export(array $rows): string;
}

Le singole implementazioni controllano la propria posizione nella collezione con #[AsTaggedItem], che imposta una priorità (la più alta viene eseguita per prima) e un indice stringa opzionale per l'accesso tramite chiave.

src/Export/CsvExporter.phpphp
namespace App\Export;

use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;

// Higher priority services appear earlier in the injected iterable
#[AsTaggedItem(priority: 10)]
final class CsvExporter implements ExporterInterface
{
    public function supports(string $format): bool
    {
        return $format === 'csv';
    }

    public function export(array $rows): string
    {
        return implode("\n", array_map(fn ($r) => implode(',', $r), $rows));
    }
}

Il servizio consumatore raccoglie ogni exporter taggato in un iterable ordinato usando #[AutowireIterator]. Poi percorre la collezione e delega alla prima strategia che supporta il formato richiesto.

src/Export/ExportManager.phpphp
namespace App\Export;

use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;

final class ExportManager
{
    // Collects all services tagged 'app.exporter', ordered by priority
    public function __construct(
        #[AutowireIterator('app.exporter')]
        private readonly iterable $exporters,
    ) {}

    public function export(string $format, array $rows): string
    {
        foreach ($this->exporters as $exporter) {
            if ($exporter->supports($format)) {
                return $exporter->export($rows);
            }
        }

        throw new \InvalidArgumentException("No exporter for format: $format");
    }
}

Quando la collezione è grande o costosa da costruire, #[AutowireLocator] è l'alternativa lazy: inietta un container PSR-11 che istanzia un servizio taggato solo quando viene effettivamente recuperato per chiave. Questa distinzione tra iterazione eager e lookup lazy conta per i servizi che mantengono connessioni o uno stato pesante.

Autoconfiguration, compiler pass e servizi lazy

L'autoconfiguration è il meccanismo che legge attributi come #[AutoconfigureTag] e #[AsTaggedItem] e li applica durante la compilazione. È il motivo per cui implementare EventSubscriberInterface basta a registrare un event subscriber con zero configurazione. Per trasformazioni che gli attributi non possono esprimere, un compiler pass si aggancia direttamente alla costruzione del container e modifica le definizioni prima che la cache venga scritta. Il codice sorgente del componente DependencyInjection su GitHub mostra come il framework stesso utilizzi internamente decine di questi pass.

I grafi sensibili alle prestazioni traggono vantaggio dai servizi lazy. Marcare un argomento con #[Autowire(lazy: true)] rimanda l'istanziazione fino al primo effettivo utilizzo della dipendenza, appoggiandosi ai lazy object nativi di PHP 8.4 anziché a una libreria di proxy generata.

src/Service/ReportBuilder.phpphp
namespace App\Service;

use App\Pdf\PdfRenderer;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class ReportBuilder
{
    public function __construct(
        // PdfRenderer is only instantiated on first actual use
        #[Autowire(lazy: true)]
        private readonly PdfRenderer $renderer,
    ) {}

    public function buildMonthly(): string
    {
        // If no method touches $this->renderer, it is never constructed
        return $this->renderer->render('monthly-report');
    }
}

I lazy object nativi sono arrivati con PHP 8.4 e vengono trattati in dettaglio nella guida su Symfony 8 e i lazy object di PHP 8.4. Il vantaggio è un container che resta snello: collaboratori pesanti come i renderer PDF, i client di SDK di terze parti o i generatori di report non costano nulla finché il percorso di codice che ne ha bisogno non viene effettivamente eseguito.

Symfony DI: domande e risposte da colloquio

Una domanda ricorrente sulla DI di Symfony ai colloqui riguarda la differenza tra autowiring e autoconfiguration. L'autowiring risolve gli argomenti del costruttore di un singolo servizio in base al tipo, mentre l'autoconfiguration applica tag, chiamate di metodo e impostazioni a un servizio in base alle interfacce che implementa. Sono interruttori indipendenti: un servizio può avere l'autowiring senza essere autoconfigured, e viceversa.

Un'altra domanda frequente è perché il container distingua tra servizi pubblici e privati. I servizi sono privati per impostazione predefinita, il che significa che non possono essere recuperati direttamente dal container con $container->get() e possono solo essere iniettati. I servizi privati possono essere inlineati ed eliminati per ottimizzazione durante la compilazione, il che è più veloce; i servizi pubblici devono restare recuperabili, quindi l'ottimizzatore li lascia intatti. Recuperare i servizi a mano dal container è considerato un anti-pattern proprio perché vanifica questa ottimizzazione e nasconde le dipendenze.

Le dipendenze circolari sono una trappola classica e una frequente domanda di approfondimento. Se il servizio A ha bisogno di B nel suo costruttore e B ha bisogno di A, il container non può costruire nessuno dei due e segnala un riferimento circolare in fase di compilazione. Le soluzioni standard consistono nel rompere il ciclo estraendo il comportamento condiviso in un terzo servizio, nel passare un lato all'iniezione lazy con #[Autowire(lazy: true)] così che la dipendenza sia risolta solo alla prima chiamata, oppure nell'usare la setter injection affinché l'oggetto esista prima che il suo collaboratore venga collegato. Riconoscere perché il ciclo è un design smell, e non solo come metterlo a tacere, è ciò che gli intervistatori ascoltano davvero.

Gli intervistatori sondano anche i tempi della risoluzione del container. Poiché Symfony compila il container una sola volta e lo mette in cache, una dipendenza non risolvibile emerge durante il warmup della cache anziché al momento della richiesta. Questa garanzia è una diretta conseguenza del modello a fase di compilazione ed è una delle ragioni per cui le grandi applicazioni Symfony restano prevedibili sotto carico. Per un insieme più ampio di queste domande, il percorso tecnologico su Symfony le raggruppa per componente e livello di difficoltà.

Conclusione

  • Lasciare lavorare il container: registrare le classi sotto src/, tipizzare le dipendenze nel costruttore e saltare del tutto il collegamento manuale in YAML.
  • Ricorrere a #[Autowire] solo per i non-servizi: parametri, variabili d'ambiente e specifici service id.
  • Risolvere l'ambiguità delle interfacce con #[AsAlias] per il predefinito e #[Autowire(service: ...)] o #[Target] per le scelte esplicite.
  • Guidare i pattern strategy e plugin con #[AutoconfigureTag], le priorità di #[AsTaggedItem] e #[AutowireIterator]; passare a #[AutowireLocator] quando il lookup lazy è preferibile all'iterazione eager.
  • Rimandare i collaboratori costosi con #[Autowire(lazy: true)], che usa i lazy object nativi di PHP 8.4.
  • Mantenere i servizi privati e iniettati anziché recuperati, così che il compilatore possa inlineare e ottimizzare il grafo.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#symfony
#dependency injection
#service container
#autowiring
#php
#di

Condividi

Articoli correlati