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.