Passage des dépendances

Les arguments, ou “dépendances” dans la terminologie DI, peuvent être transmis aux classes de la manière suivante :

  • passage par constructeur
  • en passant par une méthode (appelée setter)
  • en définissant une propriété
  • par méthode, annotation ou attribut injecter.

Nous allons maintenant illustrer les différentes variantes par des exemples concrets.

Injection de constructeur

Les dépendances sont transmises comme arguments au constructeur lors de la création de l'objet :

class MyClass
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

$obj = new MyClass($cache);

Cette forme est utile pour les dépendances obligatoires dont la classe a absolument besoin pour fonctionner, car sans elles, l'instance ne peut être créée.

Depuis PHP 8.0, nous pouvons utiliser une forme plus courte de notation qui est fonctionnellement équivalente (constructor property promotion) :

// PHP 8.0
class MyClass
{
	public function __construct(
		private Cache $cache,
	) {
	}
}

Depuis PHP 8.1, une propriété peut être marquée avec un drapeau readonly qui déclare que le contenu de la propriété ne changera pas :

// PHP 8.1
class MyClass
{
	public function __construct(
		private readonly Cache $cache,
	) {
	}
}

Le conteneur DI passe les dépendances au constructeur automatiquement en utilisant le câblage automatique. Les arguments qui ne peuvent pas être passés de cette façon (par exemple, les chaînes de caractères, les nombres, les booléens) s'écrivent dans la configuration.

L'enfer des constructeurs

Le terme constructor hell fait référence à une situation où un enfant hérite d'une classe parentale dont le constructeur nécessite des dépendances, et où l'enfant nécessite également des dépendances. Il doit également reprendre et transmettre les dépendances du parent :

abstract class BaseClass
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

final class MyClass extends BaseClass
{
	private Database $db;

	// ⛔ CONSTRUCTOR HELL
	public function __construct(Cache $cache, Database $db)
	{
		parent::__construct($cache);
		$this->db = $db;
	}
}

Le problème se pose lorsque nous voulons modifier le constructeur de la classe BaseClass, par exemple lorsqu'une nouvelle dépendance est ajoutée. Il faut alors modifier tous les constructeurs des enfants. Ce qui rend une telle modification infernale.

Comment éviter cela ? La solution est de prioriser la composition sur l'héritage.

Concevons donc le code différemment. Nous éviterons les classes abstraites Base*. Au lieu que MyClass obtienne une fonctionnalité en héritant de BaseClass, cette fonctionnalité lui sera transmise en tant que dépendance :

final class SomeFunctionality
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

final class MyClass
{
	private SomeFunctionality $sf;
	private Database $db;

	public function __construct(SomeFunctionality $sf, Database $db) // ✅
	{
		$this->sf = $sf;
		$this->db = $db;
	}
}

Injection de setter

Les dépendances sont transmises en appelant une méthode qui les stocke dans une propriété privée. La convention de dénomination habituelle pour ces méthodes est de la forme set*(), c'est pourquoi elles sont appelées “setters”, mais elles peuvent bien sûr être appelées autrement.

class MyClass
{
	private Cache $cache;

	public function setCache(Cache $cache): void
	{
		$this->cache = $cache;
	}
}

$obj = new MyClass;
$obj->setCache($cache);

Cette méthode est utile pour les dépendances facultatives qui ne sont pas nécessaires au fonctionnement de la classe, car il n'est pas garanti que l'objet les recevra effectivement (c'est-à-dire que l'utilisateur appellera la méthode).

En même temps, cette méthode permet d'appeler le setter à plusieurs reprises pour modifier la dépendance. Si cela n'est pas souhaitable, ajoutez une vérification à la méthode, ou à partir de PHP 8.1, marquez la propriété $cache avec le drapeau readonly.

class MyClass
{
	private Cache $cache;

	public function setCache(Cache $cache): void
	{
		if ($this->cache) {
			throw new RuntimeException('The dependency has already been set');
		}
		$this->cache = $cache;
	}
}

L'appel au setter est défini dans la configuration du conteneur DI dans la section setup. Ici aussi, le passage automatique des dépendances est utilisé par autowiring :

services:
	-	create: MyClass
		setup:
			- setCache

Injection de propriétés

Les dépendances sont transmises directement à la propriété :

class MyClass
{
	public Cache $cache;
}

$obj = new MyClass;
$obj->cache = $cache;

Cette méthode est considérée comme inappropriée car la propriété doit être déclarée comme public. Par conséquent, nous n'avons aucun contrôle sur le fait que la dépendance passée sera effectivement du type spécifié (c'était vrai avant PHP 7.4) et nous perdons la possibilité de réagir à la dépendance nouvellement assignée avec notre propre code, par exemple pour empêcher des changements ultérieurs. En même temps, la propriété devient une partie de l'interface publique de la classe, ce qui n'est pas forcément souhaitable.

Le paramétrage de la variable est défini dans la configuration du conteneur DI dans la section setup:

services:
	-	create: MyClass
		setup:
			- $cache = @\Cache

Injecter

Alors que les trois méthodes précédentes sont généralement valables dans tous les langages orientés objet, l'injection par méthode, annotation ou attribut inject est spécifique aux présentateurs Nette. Elles font l'objet d'un chapitre distinct.

Quelle voie choisir ?

  • le constructeur convient aux dépendances obligatoires dont la classe a besoin pour fonctionner
  • le setter, quant à lui, convient aux dépendances optionnelles, ou aux dépendances qui peuvent être modifiées.
  • les variables publiques ne sont pas recommandées
version: 3.x