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: implementa OrderRepositoryInterface con un array PHP. save() aggiunge all'array. findById() cerca nell'array. Zero database, zero latenza.
  • SpyMailer: implementa MailerInterface registrando 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 OrderService via Reflection
  • Vede che serve un OrderRepositoryInterface, lo risolve a PgsqlOrderRepository
  • Crea ricorsivamente tutte le dipendenze necessarie
  • Restituisce un OrderService completamente 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 UserRepositoryInterface con 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.

RaccontameloRispondo entro un giorno lavorativo.
Condividi:LinkedIn

Ricevi il prossimo articolo

Una mail per ogni articolo nuovo, nient'altro. Ti disiscrivi con un clic dal link in fondo a ogni email.