Dependency Injection: il pattern che rende tutto il resto possibile
Pillar article sulla Dependency Injection: perché iniettare le dipendenze è il fondamento di testabilità, flessibilità e buon design in PHP.
5 min di letturaPHP 8.4
Luigi Iadicola, Programmatore - Napoli
In questo articolo
Il problema: classi che creano le proprie dipendenze
Una classe OrderService ha bisogno di un OrderRepository per accedere ai dati e di un Mailer per inviare conferme. L'approccio più istintivo è crearli internamente: $this->repository = new PgsqlOrderRepository() e $this->mailer = new SmtpMailer(). Sembra semplice, ma ha conseguenze profonde.
Primo: OrderService è accoppiato al cemento. Sa che il repository è PostgreSQL e il mailer è SMTP. Se vuoi usare MySQL o un mock per i test, devi modificare OrderService. Secondo: non puoi testare OrderService senza un database e un server SMTP reali. Terzo: se PgsqlOrderRepository a sua volta crea le proprie dipendenze, hai una catena di creazione che nessuno controlla centralmente. Ogni classe è responsabile di crearsi il proprio mondo — e questo rende il sistema rigido, fragile e difficile da testare.
Cos'è la Dependency Injection
La Dependency Injection (DI) è un principio semplice: una classe non crea le proprie dipendenze, le riceve dall'esterno. Invece di $this->repository = new PgsqlOrderRepository(), il costruttore dichiara: public function __construct(private readonly OrderRepositoryInterface $repository, private readonly MailerInterface $mailer). Chi crea OrderService decide quale implementazione passare.
La DI non è un framework, non è un container, non è una libreria. È un principio di design: le dipendenze vengono iniettate, non create internamente. Esistono tre forme:
- Constructor Injection: le dipendenze vengono passate nel costruttore. È la forma preferita perché rende le dipendenze esplicite e immutabili: la classe non può esistere senza le sue dipendenze.
- Setter Injection: le dipendenze vengono passate tramite metodi setter. Utile per dipendenze opzionali, ma rende possibile l'uso della classe in uno stato incompleto.
- Method Injection: la dipendenza viene passata come parametro del metodo che la usa. Utile quando la dipendenza varia ad ogni chiamata.
Esempio teorico: un servizio ordini testabile
Senza DI, testare OrderService richiede un database reale con dati di test e un server SMTP (o un mock complesso). Con DI, il test crea implementazioni fake:
InMemoryOrderRepository: implementaOrderRepositoryInterfacecon un array PHP.save()aggiunge all'array.findById()cerca nell'array. Zero database, zero latenza.SpyMailer: implementaMailerInterfaceregistrando ogni email in un array interno. Il test verifica:$this->assertCount(1, $mailer->getSentEmails()). Zero SMTP, zero email reali.
Il test diventa: $service = new OrderService(new InMemoryOrderRepository(), new SpyMailer()); $service->cancel($orderId); $this->assertTrue(...). È un test unitario puro: testa la logica di cancellazione senza dipendenze esterne, senza database, senza rete. Gira in millisecondi.
Il test dice la verità sul design
Se una classe è difficile da testare, quasi sempre è perché le dipendenze non sono iniettate. Il test è il primo "utente" della classe, e se l'utente fa fatica, il design ha un problema. La DI non è un requisito per il testing — è un requisito per il buon design, e il testing è la prova empirica.
Il Container: automazione della DI
Con la DI manuale, chi crea OrderService deve conoscere tutte le sue dipendenze e le dipendenze delle dipendenze. In un'applicazione con 50 classi, questa catena diventa lunga e ripetitiva. Il DI Container automatizza la risoluzione:
- Registri le associazioni:
OrderRepositoryInterface→PgsqlOrderRepository - Il container legge il costruttore di
OrderServicevia Reflection - Vede che serve un
OrderRepositoryInterface, lo risolve aPgsqlOrderRepository - Crea ricorsivamente tutte le dipendenze necessarie
- Restituisce un
OrderServicecompletamente assemblato
Il container è un lusso, non una necessità. Per progetti piccoli, la DI manuale (creare gli oggetti nel bootstrap) è perfettamente valida. Per progetti con decine di servizi e dipendenze profonde, il container elimina il boilerplate di assemblaggio.
DI e il Dependency Inversion Principle
La Dependency Injection è la tecnica. Il Dependency Inversion Principle (la D di SOLID) è il principio: "i moduli di alto livello non dovrebbero dipendere da moduli di basso livello. Entrambi dovrebbero dipendere da astrazioni." La DI è il meccanismo pratico per realizzare questo principio.
Senza DI, OrderService (alto livello) dipende da PgsqlOrderRepository (basso livello). Con DI, OrderService dipende da OrderRepositoryInterface (astrazione), e PgsqlOrderRepository implementa quell'astrazione. La direzione della dipendenza si è invertita: il modulo di basso livello dipende dall'astrazione definita dal modulo di alto livello, non viceversa.
Errori comuni con la DI
- Service Locator: iniettare il container stesso e usarlo per risolvere le dipendenze a runtime. Nasconde le dipendenze reali e rende il codice opaco. Le dipendenze devono essere nel costruttore, non risolte a runtime.
- Troppe dipendenze: se un costruttore ha 8 parametri, il problema non è la DI — è che la classe ha troppe responsabilità. La DI rende visibile il problema, non lo causa.
- Interfacce inutili: creare un'interfaccia per ogni classe "perché la DI lo richiede" è cargo cult. L'interfaccia serve quando ci sono (o ci saranno realisticamente) multiple implementazioni. Una
UserRepositoryInterfacecon una sola implementazione è burocrazia — a meno che non serva per il testing, e allora ha un motivo concreto. - Auto-wiring magico: affidarsi ciecamente alla risoluzione automatica del container rende invisibili le dipendenze. È comodo ma va bilanciato con la leggibilità.
Quando usare la Dependency Injection
- Sempre per le dipendenze di servizio (repository, mailer, logger, cache): sono le dipendenze che cambiano tra ambienti (produzione, test, staging)
- Sempre quando vuoi che la classe sia testabile con mock o fake
- Non necessaria per value objects e DTO: creare un
new Money(100, 'EUR')internamente è perfettamente valido - Non necessaria per factory di oggetti semplici: non serve iniettare una factory se la creazione è banale
La Dependency Injection non è un pattern opzionale da adottare quando si diventa esperti: è il fondamento che rende possibili tutti gli altri pattern. Strategy, Repository, Observer, Decorator — tutti funzionano perché le dipendenze sono iniettate, non create internamente. Senza DI, i pattern diventano rigidi e non sostituibili. Con DI, diventano componenti intercambiabili di un sistema flessibile.
Hai lo stesso problema in azienda?
Raccontami cosa non va e da quando. Guardo la situazione con te prima di proporre qualsiasi intervento.
Ricevi il prossimo articolo
Una mail per ogni articolo nuovo, nient'altro. Ti disiscrivi con un clic dal link in fondo a ogni email.