Passage des dépendances
Les arguments, ou ‘dépendances’ dans la terminologie DI, peuvent être passés aux classes des principales façons suivantes :
- Injection par le constructeur
- Injection par méthode (dite injection par setter)
- Injection dans une propriété
- À l'aide de la méthode
inject*()ou de l'attribut#[Inject]
Illustrons chaque variante par des exemples concrets.
Injection par le constructeur
Les dépendances sont fournies comme arguments du constructeur au moment où l'objet est instancié :
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Cette approche convient aux dépendances obligatoires, dont la classe a absolument besoin pour fonctionner, car sans elles l'instance ne peut pas être créée.
Depuis PHP 8.0, nous pouvons utiliser une notation plus courte (promotion des propriétés du constructeur), fonctionnellement équivalente :
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Depuis PHP 8.1, une propriété peut être marquée du drapeau readonly, qui déclare que sa valeur ne changera
plus après l'initialisation :
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
Le conteneur DI passe les dépendances au constructeur automatiquement grâce à l'autowiring. Les arguments qui ne peuvent pas être fournis de cette façon (par exemple des chaînes, des nombres, des booléens) sont indiqués dans la configuration.
Constructor hell
Le terme constructor hell décrit une situation où une classe enfant hérite d'une classe parente dont le constructeur exige des dépendances, alors que la classe enfant en exige elle aussi. Elle doit alors accepter et transmettre également 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 surgit lorsque nous voulons modifier le constructeur de BaseClass, par exemple quand une nouvelle
dépendance s'y ajoute. Il devient alors nécessaire de modifier aussi tous les constructeurs des classes enfants. Ce qui
transforme une telle modification en enfer.
Comment l'éviter ? La solution consiste à préférer la composition à l'héritage.
Nous concevons donc le code autrement. Nous éviterons les classes abstraites
Base*. Au lieu que MyClass obtienne une certaine fonctionnalité en héritant de BaseClass,
cette fonctionnalité lui sera passée comme 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 par setter
Les dépendances sont fournies par l'appel d'une méthode qui les stocke dans une propriété privée. La convention de nommage
habituelle de ces méthodes suit le modèle set*(), d'où leur nom de setters, mais elles peuvent bien sûr être
nommées autrement.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Cette approche convient aux dépendances facultatives, qui ne sont pas indispensables au fonctionnement de la classe, car rien ne garantit que l'objet recevra effectivement la dépendance (c'est-à-dire que l'appelant invoquera la méthode).
En même temps, cette méthode permet d'appeler le setter à plusieurs reprises pour changer la dépendance. Si ce n'est pas
souhaitable, ajoutez une vérification dans la méthode ou, depuis PHP 8.1, marquez la propriété $cache du drapeau
readonly.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if (isset($this->cache)) {
throw new RuntimeException('La dépendance a déjà été définie');
}
$this->cache = $cache;
}
}
L'appel du setter se définit dans la configuration du conteneur DI, dans la clé setup. Là aussi, la fourniture automatique des dépendances par autowiring est utilisée :
services:
- create: MyClass
setup:
- setCache
Injection dans une propriété
Les dépendances sont fournies par écriture directe dans une propriété de l'objet :
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 public. Nous perdons
donc le contrôle sur le fait que la dépendance passée est bien du type requis (c'était particulièrement vrai avant les
déclarations de type des propriétés en PHP 7.4), ainsi que la possibilité de réagir à une dépendance nouvellement affectée
par une logique propre, par exemple pour empêcher une modification ultérieure. En même temps, la propriété devient partie de
l'API publique de la classe, ce qui n'est pas forcément voulu.
L'affectation de la propriété se définit dans la configuration du conteneur DI, dans la section setup :
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
Alors que les trois approches précédentes s'appliquent de façon générale dans tous les langages orientés objet,
l'injection par les méthodes inject*() ou par l'attribut #[Inject] est typiquement utilisée avec les
presenters Nette, où elle est activée par défaut ; tout autre service peut l'activer via inject: true. Elles sont traitées
dans un chapitre distinct.
Quelle méthode choisir ?
- Le constructeur convient aux dépendances obligatoires, dont la classe a absolument besoin pour fonctionner.
- Le setter, à l'inverse, convient aux dépendances facultatives, ou à celles qu'il faudra peut-être changer par la suite.
- Les propriétés publiques ne sont généralement pas recommandées.