Che cos'è un container DI?

Un container di dependency injection (DIC o container DI) è un oggetto che si occupa di istanziare e configurare altri oggetti (chiamati servizi).

Vi sorprenderà, ma in molti casi non avete bisogno di un container di dependency injection per sfruttare i vantaggi della dependency injection (in breve DI). In fondo, anche nel capitolo introduttivo abbiamo mostrato esempi concreti di DI, e nessun container è stato necessario.

Quando però si gestisce un gran numero di oggetti con dipendenze complesse, un container DI diventa molto utile. È spesso il caso delle applicazioni web costruite su un framework.

Nel capitolo precedente abbiamo introdotto le classi Article ed EditController. Entrambe hanno dipendenze, cioè il database e la factory ArticleFactory. E per queste classi creeremo ora un container. Naturalmente creare un container per un esempio così semplice è eccessivo, ma lo creeremo per mostrare che aspetto ha e come funziona.

Ecco un semplice container scritto a mano per l'esempio qui sopra:

class Container
{
	public function createDatabase(): Nette\Database\Connection
	{
		return new Nette\Database\Connection('mysql:', 'root', '***');
	}

	public function createArticleFactory(): ArticleFactory
	{
		return new ArticleFactory($this->createDatabase());
	}

	public function createEditController(): EditController
	{
		return new EditController($this->createArticleFactory());
	}
}

L'uso avrebbe questo aspetto:

$container = new Container;
$controller = $container->createEditController();

Chiediamo semplicemente l'oggetto al container, senza bisogno di sapere come crearlo o quali siano le sue dipendenze: se ne occupa tutto il container. Le dipendenze vengono iniettate automaticamente dal container. È questa la sua forza.

Al momento il container ha tutte le informazioni scritte a mano nel codice. Facciamo quindi il passo successivo e aggiungiamo dei parametri, per rendere il container davvero utile:

class Container
{
	public function __construct(
		private array $parameters,
	) {
	}

	public function createDatabase(): Nette\Database\Connection
	{
		return new Nette\Database\Connection(
			$this->parameters['db.dsn'],
			$this->parameters['db.user'],
			$this->parameters['db.password'],
		);
	}

	// ...
}

$container = new Container([
	'db.dsn' => 'mysql:',
	'db.user' => 'root',
	'db.password' => '***',
]);

I lettori più attenti noteranno un problema. Ogni volta che otteniamo un oggetto EditController vengono create anche nuove istanze di ArticleFactory e della connessione al database. Non è affatto quello che vogliamo.

Aggiungeremo quindi un metodo getService(), che restituirà sempre le stesse istanze:

class Container
{
	private array $services = [];

	public function __construct(
		private array $parameters,
	) {
	}

	public function getService(string $name): object
	{
		if (!isset($this->services[$name])) {
			// getService('Database') chiamerà createDatabase()
			$method = 'create' . $name;
			$this->services[$name] = $this->$method();
		}
		return $this->services[$name];
	}

	// ...
}

Alla prima chiamata, per esempio $container->getService('Database'), chiama createDatabase() per creare l'oggetto database, lo salva nell'array $services e lo restituisce. Alle chiamate successive restituisce direttamente l'istanza già salvata.

Modifichiamo anche il resto del container perché usi getService():

class Container
{
	// ...

	public function createArticleFactory(): ArticleFactory
	{
		return new ArticleFactory($this->getService('Database'));
	}

	public function createEditController(): EditController
	{
		return new EditController($this->getService('ArticleFactory'));
	}
}

A proposito, il termine servizio indica qualsiasi oggetto gestito dal container. Da qui il nome del metodo getService().

Fatto. Abbiamo un container DI pienamente funzionante! E possiamo usarlo:

$container = new Container([
	'db.dsn' => 'mysql:',
	'db.user' => 'root',
	'db.password' => '***',
]);

$controller = $container->getService('EditController');
$database = $container->getService('Database');

Come vedete, scrivere un DIC non è difficile. Vale la pena notare che gli oggetti stessi non sanno che a crearli è un container. Di conseguenza è possibile creare così qualsiasi oggetto PHP, senza modificarne il codice sorgente.

Creare e mantenere a mano la classe del container può però diventare rapidamente un incubo. Nel prossimo capitolo parleremo quindi di Nette DI Container, che sa generarsi e aggiornarsi quasi automaticamente.

versione: 3.x