¿Qué es un contenedor DI?

Un contenedor de inyección de dependencias (DIC) es una clase que puede instanciar y configurar objetos.

Puede sorprenderte, pero en muchos casos no necesitas un contenedor de inyección de dependencias para aprovechar las ventajas de la inyección de dependencias (DI para abreviar). Después de todo, incluso en capítulo anterior mostramos ejemplos específicos de DI y no se necesitaba ningún contenedor.

Sin embargo, si necesitas gestionar un gran número de objetos diferentes con muchas dependencias, un contenedor de inyección de dependencias será realmente útil. Que es quizás el caso de las aplicaciones web construidas sobre un framework.

En el capítulo anterior, introdujimos las clases Article y UserController. Ambas tienen algunas dependencias, a saber, la base de datos y la fábrica ArticleFactory. Y para estas clases, ahora vamos a crear un contenedor. Por supuesto, para un ejemplo tan simple, no tiene sentido tener un contenedor. Pero vamos a crear uno para mostrar cómo se ve y funciona.

He aquí un simple contenedor hardcodeado 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 createUserController(): UserController
	{
		return new UserController($this->createArticleFactory());
	}
}

El uso sería así

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

Simplemente pedimos el objeto al contenedor y ya no necesitamos saber nada sobre cómo crearlo o cuáles son sus dependencias; el contenedor sabe todo eso. Las dependencias son inyectadas automáticamente por el contenedor. Ese es su poder.

Hasta ahora, el contenedor tiene todo codificado. Así que damos el siguiente paso y añadimos parámetros para hacer el contenedor realmente ú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 avispados habrán notado un problema. Cada vez que obtengo un objeto UserController, también se crea una nueva instancia ArticleFactory y base de datos. Definitivamente no queremos eso.

Así que añadimos un método getService() que devolverá las mismas instancias una y otra vez:

class Container
{
	private array $services = [];

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

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

	// ...
}

La primera llamada a, por ejemplo, $container->getService('Database') hará que createDatabase() cree un objeto base de datos, que almacenará en el array $services y lo devolverá directamente en la siguiente llamada.

También modificamos el resto del contenedor para que utilice getService():

class Container
{
	// ...

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

	public function createUserController(): UserController
	{
		return new UserController($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 completamente funcional! Y ya podemos usarlo:

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

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

Como puedes ver, no es difícil escribir un DIC. Es destacable que los propios objetos no saben que un contenedor los está creando. Por lo tanto, es posible crear cualquier objeto en PHP de esta manera sin afectar su código fuente.

Crear y mantener manualmente una clase contenedora puede convertirse rápidamente en una pesadilla. Por ello, en el próximo capítulo hablaremos de Nette DI Container, que puede generarse y actualizarse casi automáticamente.