Qu'est-ce qu'un conteneur DI ?

Un conteneur d'injection de dépendances (DIC ou conteneur DI) est un objet chargé d'instancier et de configurer d'autres objets (appelés services).

Cela va peut-être vous surprendre, mais dans bien des cas, vous n'avez pas besoin d'un conteneur d'injection de dépendances pour profiter des avantages de l'injection de dépendances (DI en abrégé). Après tout, même dans le chapitre d'introduction, nous avons montré des exemples concrets de DI, et aucun conteneur n'était nécessaire.

En revanche, dès qu'il faut gérer un grand nombre d'objets aux dépendances complexes, un conteneur DI devient très utile. C'est souvent le cas des applications web construites sur un framework.

Dans le chapitre précédent, nous avons présenté les classes Article et EditController. Toutes deux ont des dépendances, à savoir la base de données et la factory ArticleFactory. Et c'est pour ces classes que nous allons maintenant créer un conteneur. Bien sûr, créer un conteneur pour un exemple aussi simple est exagéré. Mais nous allons en créer un pour montrer à quoi il ressemble et comment il fonctionne.

Voici un conteneur simple, écrit en dur, pour l'exemple ci-dessus :

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'utilisation ressemblerait à ceci :

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

Nous demandons simplement l'objet au conteneur, sans avoir besoin de savoir comment le créer ni quelles sont ses dépendances ; le conteneur s'occupe de tout. Les dépendances sont injectées automatiquement par le conteneur. C'est là sa force.

Pour l'instant, le conteneur a toutes les informations écrites en dur. Passons donc à l'étape suivante et ajoutons des paramètres pour rendre le conteneur vraiment 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' => '***',
]);

Les lecteurs attentifs remarqueront un problème. Chaque fois que nous récupérons un objet EditController, de nouvelles instances de ArticleFactory et de la connexion à la base de données sont également créées. Ce n'est vraiment pas ce que nous voulons.

Nous allons donc ajouter une méthode getService() qui renverra toujours les mêmes instances :

class Container
{
	private array $services = [];

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

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

	// ...
}

Au premier appel, par exemple $container->getService('Database'), elle appelle createDatabase() pour créer l'objet de base de données, le stocke dans le tableau $services et le renvoie. Lors des appels suivants, elle renvoie directement l'instance déjà stockée.

Nous modifions aussi le reste du conteneur pour qu'il utilise getService() :

class Container
{
	// ...

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

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

Au passage, le terme service désigne n'importe quel objet géré par le conteneur. D'où le nom de la méthode getService().

C'est fait. Nous avons un conteneur DI pleinement fonctionnel ! Et nous pouvons l'utiliser :

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

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

Comme vous le voyez, écrire un DIC n'a rien de difficile. Il est bon de noter que les objets eux-mêmes ignorent qu'un conteneur les crée. Il est donc possible de créer ainsi n'importe quel objet PHP sans modifier son code source.

Créer et maintenir à la main la classe du conteneur peut vite tourner au cauchemar. C'est pourquoi, dans le chapitre suivant, nous parlerons du Nette DI Container, qui sait se générer et se mettre à jour presque automatiquement.

version: 3.x