Passage des dépendances
Les arguments, ou dans la terminologie DI “dépendances”, peuvent être passés aux classes de ces manières principales :
- passage par constructeur
- passage par méthode (appelée setter)
- assignation à une variable
- méthode, annotation ou attribut inject
Nous allons maintenant montrer les différentes variantes avec des exemples concrets.
Passage par constructeur
Les dépendances sont passées au moment de la création de l'objet comme arguments du constructeur :
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Cette forme convient aux dépendances obligatoires dont la classe a absolument besoin pour fonctionner, car sans elles, l'instance ne pourra pas être créée.
Depuis PHP 8.0, nous pouvons utiliser une forme d'écriture plus courte (constructor property promotion), qui est fonctionnellement équivalente :
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Depuis PHP 8.1, la variable peut être marquée avec l'indicateur readonly
, qui déclare que le contenu de la
variable ne changera plus :
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
Le conteneur DI passe automatiquement les dépendances au constructeur via l'autowiring. Les arguments qui ne peuvent pas être passés de cette manière (par exemple, les chaînes de caractères, les nombres, les booléens) sont écrits dans la configuration.
Constructor hell
Le terme constructor hell désigne la situation où un enfant hérite d'une classe parente dont le constructeur requiert des dépendances, et en même temps, l'enfant requiert également des dépendances. Il doit alors reprendre et passer également celles 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 survient lorsque nous voulons changer le constructeur de la classe BaseClass
, par exemple lorsqu'une
nouvelle dépendance est ajoutée. Il est alors nécessaire de modifier également tous les constructeurs des enfants. Ce qui
transforme une telle modification en enfer.
Comment éviter cela ? La solution est de préférer la composition plutôt que l'héritage.
Nous concevrons donc le code différemment. Nous éviterons les classes abstraites
Base*
. Au lieu que MyClass
obtienne une certaine fonctionnalité en héritant de BaseClass
,
elle se fera passer cette fonctionnalité comme une 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;
}
}
Passage par setter
Les dépendances sont passées en appelant une méthode qui les stocke dans une variable privée. La convention habituelle pour
nommer ces méthodes est la forme set*()
, c'est pourquoi on les appelle setters, mais elles peuvent bien sûr
s'appeler autrement.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Cette méthode convient aux dépendances facultatives qui ne sont pas nécessaires au fonctionnement de la classe, car il n'est pas garanti que l'objet reçoive réellement la dépendance (c'est-à-dire que l'utilisateur appelle la méthode).
En même temps, cette méthode permet d'appeler le setter de manière répétée et de changer ainsi la dépendance. Si cela
n'est pas souhaité, nous ajoutons un contrôle dans la méthode, ou à partir de PHP 8.1, nous marquons la propriété
$cache
avec l'indicateur readonly
.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if (isset($this->cache)) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
L'appel du setter est défini dans la configuration du conteneur DI dans la clé setup. Ici aussi, le passage automatique des dépendances via l'autowiring est utilisé :
services:
- create: MyClass
setup:
- setCache
Assignation à une variable
Les dépendances sont passées en écrivant directement dans la variable membre :
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Cette méthode est considérée comme inappropriée car la variable membre 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 réellement du type donné (valable 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 un changement ultérieur. En même temps, la variable devient partie intégrante de l'interface publique de la
classe, ce qui peut ne pas être souhaitable.
L'assignation de la variable est définie dans la configuration du conteneur DI dans la section setup :
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
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 uniquement aux presenters dans Nette. Un chapitre distinct leur est consacré.
Quelle méthode choisir ?
- Le constructeur convient aux dépendances obligatoires dont la classe a absolument besoin pour fonctionner.
- Le setter convient au contraire aux dépendances facultatives, ou aux dépendances qu'il est possible de modifier ultérieurement.
- Les variables publiques ne sont pas appropriées.