Autowiring
L'autowiring est une fonctionnalité formidable qui peut automatiquement passer les services requis au constructeur et à d'autres méthodes, de sorte que nous n'avons pas du tout besoin de les écrire. Cela vous fait gagner beaucoup de temps.
Grâce à cela, nous pouvons omettre la grande majorité des arguments lors de l'écriture des définitions de service. Au lieu de :
services:
articles: Model\ArticleRepository(@database, @cache.storage)
Il suffit d'écrire :
services:
articles: Model\ArticleRepository
L'autowiring est basé sur les types, donc pour qu'il fonctionne, la classe ArticleRepository
doit être définie
à peu près comme ceci :
namespace Model;
class ArticleRepository
{
public function __construct(\PDO $db, \Nette\Caching\Storage $storage)
{}
}
Pour pouvoir utiliser l'autowiring, il doit y avoir exactement un service pour chaque type dans le conteneur. S'il y en avait plus, l'autowiring ne saurait pas lequel passer et lèverait une exception :
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb: PDO('sqlite::memory:')
articles: Model\ArticleRepository # LÈVE UNE EXCEPTION, mainDb et tempDb correspondent
La solution serait soit de contourner l'autowiring et de spécifier explicitement le nom du service (c'est-à-dire
articles: Model\ArticleRepository(@mainDb)
). Mais il est plus judicieux de désactiver l'autowiring pour l'un des services, ou de préférer le premier service.
Désactivation de l'autowiring
Nous pouvons désactiver l'autowiring d'un service à l'aide de l'option autowired: no
:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb:
create: PDO('sqlite::memory:')
autowired: false # le service tempDb est exclu de l'autowiring
articles: Model\ArticleRepository # donc il passe mainDb au constructeur
Le service articles
ne lèvera pas d'exception indiquant qu'il existe deux services correspondants de type
PDO
(c'est-à-dire mainDb
et tempDb
) qui peuvent être passés au constructeur, car il ne
voit que le service mainDb
.
La configuration de l'autowiring dans Nette fonctionne différemment de Symfony, où l'option
autowire: false
indique que l'autowiring ne doit pas être utilisé pour les arguments du constructeur du service
donné. Dans Nette, l'autowiring est toujours utilisé, que ce soit pour les arguments du constructeur ou pour toute autre
méthode. L'option autowired: false
indique que l'instance du service donné ne doit être passée nulle part via
l'autowiring.
Préférence d'autowiring
Si nous avons plusieurs services du même type et que nous spécifions l'option autowired
pour l'un d'entre eux,
ce service devient le préféré :
services:
mainDb:
create: PDO(%dsn%, %user%, %password%)
autowired: PDO # devient préféré
tempDb:
create: PDO('sqlite::memory:')
articles: Model\ArticleRepository
Le service articles
ne lèvera pas d'exception indiquant qu'il existe deux services correspondants de type
PDO
(c'est-à-dire mainDb
et tempDb
), mais utilisera le service préféré, c'est-à-dire
mainDb
.
Tableau de services
L'autowiring peut également passer des tableaux de services d'un certain type. Comme il n'est pas possible d'écrire
nativement le type des éléments d'un tableau en PHP, il faut, en plus du type array
, ajouter un commentaire phpDoc
avec le type de l'élément sous la forme ClassName[]
:
namespace Model;
class ShipManager
{
/**
* @param Shipper[] $shippers
*/
public function __construct(array $shippers)
{}
}
Le conteneur DI passera alors automatiquement un tableau de services correspondant au type donné. Il omettra les services dont l'autowiring est désactivé.
Le type dans le commentaire peut également être sous la forme array<int, Class>
ou
list<Class>
. Si vous ne pouvez pas influencer la forme du commentaire phpDoc, vous pouvez passer le tableau de
services directement dans la configuration à l'aide de typed()
.
Arguments scalaires
L'autowiring ne peut injecter que des objets et des tableaux d'objets. Les arguments scalaires (par exemple, chaînes, nombres, booléens) sont écrits dans la configuration. Une alternative est de créer un objet de paramètres, qui encapsule la valeur scalaire (ou plusieurs valeurs) sous forme d'objet, lequel peut ensuite être à nouveau passé via l'autowiring.
class MySettings
{
public function __construct(
// readonly peut être utilisé depuis PHP 8.1
public readonly bool $value,
)
{}
}
Vous en faites un service en l'ajoutant à la configuration :
services:
- MySettings('any value')
Toutes les classes le demanderont ensuite via l'autowiring.
Réduction de l'autowiring
Pour les services individuels, l'autowiring peut être limité à certaines classes ou interfaces.
Normalement, l'autowiring passe le service à chaque paramètre de méthode dont le type correspond au service. La réduction signifie que nous définissons des conditions auxquelles les types spécifiés pour les paramètres de méthode doivent satisfaire pour que le service leur soit passé.
Illustrons cela par un exemple :
class ParentClass
{}
class ChildClass extends ParentClass
{}
class ParentDependent
{
function __construct(ParentClass $obj)
{}
}
class ChildDependent
{
function __construct(ChildClass $obj)
{}
}
Si nous les enregistrions tous comme services, l'autowiring échouerait :
services:
parent: ParentClass
child: ChildClass
parentDep: ParentDependent # LÈVE UNE EXCEPTION, les services parent et child correspondent
childDep: ChildDependent # l'autowiring passe le service child au constructeur
Le service parentDep
lèvera une exception
Multiple services of type ParentClass found: parent, child
, car les deux services parent
et
child
correspondent à son constructeur, et l'autowiring ne peut pas décider lequel choisir.
Pour le service child
, nous pouvons donc réduire son autowiring au type ChildClass
:
services:
parent: ParentClass
child:
create: ChildClass
autowired: ChildClass # peut aussi s'écrire 'autowired: self'
parentDep: ParentDependent # l'autowiring passe le service parent au constructeur
childDep: ChildDependent # l'autowiring passe le service child au constructeur
Maintenant, le service parent
est passé au constructeur du service parentDep
, car c'est maintenant
le seul objet correspondant. L'autowiring ne passera plus le service child
là-bas. Oui, le service
child
est toujours de type ParentClass
, mais la condition de réduction donnée pour le type de
paramètre n'est plus remplie, c'est-à-dire qu'il n'est pas vrai que ParentClass
est un supertype de
ChildClass
.
Pour le service child
, autowired: ChildClass
pourrait également être écrit comme
autowired: self
, car self
est un alias pour la classe du service actuel.
Dans la clé autowired
, il est également possible de spécifier plusieurs classes ou interfaces sous forme de
tableau :
autowired: [BarClass, FooInterface]
Essayons de compléter l'exemple avec des interfaces :
interface FooInterface
{}
interface BarInterface
{}
class ParentClass implements FooInterface
{}
class ChildClass extends ParentClass implements BarInterface
{}
class FooDependent
{
function __construct(FooInterface $obj)
{}
}
class BarDependent
{
function __construct(BarInterface $obj)
{}
}
class ParentDependent
{
function __construct(ParentClass $obj)
{}
}
class ChildDependent
{
function __construct(ChildClass $obj)
{}
}
Si nous ne limitons pas le service child
de quelque manière que ce soit, il correspondra aux constructeurs de
toutes les classes FooDependent
, BarDependent
, ParentDependent
et
ChildDependent
et l'autowiring l'y passera.
Cependant, si nous limitons son autowiring à ChildClass
en utilisant autowired: ChildClass
(ou
self
), l'autowiring ne le passera qu'au constructeur de ChildDependent
, car il nécessite un argument de
type ChildClass
et il est vrai que ChildClass
est de type ChildClass
. Aucun autre
type spécifié pour les autres paramètres n'est un supertype de ChildClass
, donc le service ne sera
pas passé.
Si nous le limitons à ParentClass
en utilisant autowired: ParentClass
, il sera à nouveau passé au
constructeur de ChildDependent
(car le ChildClass
requis est un supertype de ParentClass
)
et nouvellement aussi au constructeur de ParentDependent
, car le type ParentClass
requis est également
satisfaisant.
Si nous le limitons à FooInterface
, il sera toujours autowiré dans ParentDependent
(le
ParentClass
requis est un supertype de FooInterface
) et ChildDependent
, mais en plus aussi
dans le constructeur de FooDependent
, mais pas dans BarDependent
, car BarInterface
n'est
pas un supertype de FooInterface
.
services:
child:
create: ChildClass
autowired: FooInterface
fooDep: FooDependent # l'autowiring passe le service child au constructeur
barDep: BarDependent # LÈVE UNE EXCEPTION, aucun service ne correspond
parentDep: ParentDependent # l'autowiring passe le service child au constructeur
childDep: ChildDependent # l'autowiring passe le service child au constructeur