Che cos'è il contenitore DI?

Il dependency injection container (DIC) è una classe che può istanziare e configurare oggetti.

Può sorprendere, ma in molti casi non è necessario un contenitore per l'iniezione di dipendenza (dependency injection container) per trarre vantaggio dall'iniezione di dipendenza (DI in breve). Dopo tutto, anche nel capitolo precedente abbiamo mostrato esempi specifici di DI e non è stato necessario alcun contenitore.

Tuttavia, se è necessario gestire un gran numero di oggetti diversi con molte dipendenze, un contenitore di dependency injection sarà davvero utile. Questo è forse il caso delle applicazioni web costruite su un framework.

Nel capitolo precedente, abbiamo introdotto le classi Article e UserController. Entrambe hanno alcune dipendenze, ovvero il database e il factory ArticleFactory. Per queste classi, ora creeremo un contenitore. Naturalmente, per un esempio così semplice, non ha senso avere un contenitore. Ma ne creeremo uno per mostrare l'aspetto e il funzionamento.

Ecco un semplice contenitore codificato per l'esempio precedente:

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 createUserController(): UserController
	{
		return new UserController($this->createArticleFactory());
	}
}

L'uso sarebbe simile a questo:

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

Basta chiedere al contenitore l'oggetto e non è più necessario sapere come crearlo o quali sono le sue dipendenze; il contenitore sa tutto. Le dipendenze vengono iniettate automaticamente dal contenitore. Questo è il suo potere.

Finora, il contenitore ha tutto codificato. Quindi facciamo il passo successivo e aggiungiamo dei parametri per rendere il contenitore 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 avranno notato un problema. Ogni volta che ottengo un oggetto UserController, viene creata anche una nuova istanza ArticleFactory e un database. Questo non lo vogliamo assolutamente.

Aggiungiamo 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') chiama createDatabase()
			$method = 'create' . $name;
			$this->services[$name] = $this->$method();
		}
		return $this->services[$name];
	}

	// ...
}

La prima chiamata ad esempio a $container->getService('Database') farà sì che createDatabase() crei un oggetto database, che memorizzerà nell'array $services e lo restituirà direttamente alla chiamata successiva.

Modifichiamo anche il resto del contenitore per usare getService():

class Container
{
	// ...

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

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

A proposito, il termine servizio si riferisce a qualsiasi oggetto gestito dal contenitore. Da qui il nome del metodo getService().

Fatto. Abbiamo un contenitore DI completamente funzionante! E possiamo usarlo:

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

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

Come si può vedere, non è difficile scrivere un DI. È da notare che gli oggetti stessi non sanno che un contenitore li sta creando. Pertanto, è possibile creare qualsiasi oggetto in PHP in questo modo, senza modificare il codice sorgente.

Creare e mantenere manualmente una classe contenitore può diventare un incubo piuttosto rapidamente. Pertanto, nel prossimo capitolo parleremo di Nette DI Container, che può generare e aggiornare se stesso in modo quasi automatico.

versione: 3.x