¿Qué es un contenedor DI?
Un contenedor de inyección de dependencias (DIC o contenedor DI) es un objeto responsable de crear y configurar otros objetos (llamados servicios).
Quizá le sorprenda, pero en muchos casos no hace falta un contenedor de inyección de dependencias para aprovechar las ventajas de la inyección de dependencias (DI, para abreviar). Al fin y al cabo, incluso en el capítulo introductorio mostramos ejemplos concretos de DI y no hizo falta ningún contenedor.
Sin embargo, cuando hay que gestionar un gran número de objetos con dependencias complejas, un contenedor DI resulta muy útil. Es lo que suele ocurrir con las aplicaciones web construidas sobre un framework.
En el capítulo anterior presentamos las clases Article y EditController. Ambas tienen dependencias,
en concreto la base de datos y la factory ArticleFactory. Y para estas clases vamos a crear ahora un contenedor. Por
supuesto, crear un contenedor para un ejemplo tan simple es una exageración. Pero lo crearemos para mostrar qué aspecto tiene y
cómo funciona.
Este es un contenedor sencillo, escrito a fuego, para el ejemplo anterior:
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());
}
}
Su uso sería así:
$container = new Container;
$controller = $container->createEditController();
Simplemente le pedimos el objeto al contenedor, sin tener que saber cómo se crea ni cuáles son sus dependencias; de todo eso se ocupa el contenedor. Las dependencias las inyecta el contenedor automáticamente. En eso está su fuerza.
De momento, el contenedor tiene toda la información escrita a fuego. Así que demos el siguiente paso y añadamos parámetros para que el contenedor sea de verdad útil:
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' => '***',
]);
Los lectores más atentos habrán detectado un problema. Cada vez que obtenemos un objeto EditController se crean
también nuevas instancias de ArticleFactory y de la conexión a la base de datos. Eso desde luego no lo
queremos.
Por eso añadiremos un método getService() que devolverá siempre las mismas instancias:
class Container
{
private array $services = [];
public function __construct(
private array $parameters,
) {
}
public function getService(string $name): object
{
if (!isset($this->services[$name])) {
// getService('Database') llamará a createDatabase()
$method = 'create' . $name;
$this->services[$name] = $this->$method();
}
return $this->services[$name];
}
// ...
}
En la primera llamada, por ejemplo $container->getService('Database'), llama a createDatabase()
para crear el objeto de la base de datos, lo guarda en el array $services y lo devuelve. En las llamadas siguientes
devuelve directamente la instancia ya guardada.
Modificamos también el resto del contenedor para que use getService():
class Container
{
// ...
public function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->getService('Database'));
}
public function createEditController(): EditController
{
return new EditController($this->getService('ArticleFactory'));
}
}
Por cierto, el término servicio se refiere a cualquier objeto gestionado por el contenedor. De ahí el nombre del método
getService().
Listo. ¡Tenemos un contenedor DI plenamente funcional! Y podemos usarlo:
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
$controller = $container->getService('EditController');
$database = $container->getService('Database');
Como ve, escribir un DIC no es difícil. Conviene señalar que los propios objetos no saben que un contenedor los está creando. En consecuencia, así se puede crear cualquier objeto de PHP sin modificar su código fuente.
Crear y mantener a mano la clase del contenedor puede convertirse rápidamente en una pesadilla. Por eso, en el siguiente capítulo hablaremos del contenedor DI de Nette, que sabe generarse y actualizarse casi por completo solo.