Autowiring
L'autowiring est une fonctionnalité formidable qui passe automatiquement les services requis au constructeur et aux autres méthodes, si bien que nous n'avons pas à les indiquer explicitement. Il vous fait gagner énormément de temps.
Grâce à lui, nous pouvons omettre la grande majorité des arguments lors de l'écriture des définitions de services. Au lieu de :
services:
articles: Model\ArticleRepository(@database, @cache.storage)
Il suffit d'écrire :
services:
articles: Model\ArticleRepository
L'autowiring se guide sur les types ; pour qu'il fonctionne, la classe ArticleRepository doit donc être définie
à peu près ainsi :
namespace Model;
class ArticleRepository
{
public function __construct(\PDO $db, \Nette\Caching\Storage $storage)
{}
}
L'autowiring n'utilise jamais les noms des services. Il se guide uniquement sur le système de types de PHP, il sait donc aussi qu'une classe satisfait les interfaces qu'elle implémente et les classes dont elle hérite. De ce fait, le nom d'un service n'est qu'un identifiant auxiliaire et le renommer ne cassera rien dans l'application.
Pour pouvoir utiliser l'autowiring, il doit y avoir dans le conteneur exactement un service de chaque type. S'il y en avait plusieurs, 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
Une solution consiste à contourner l'autowiring et à indiquer explicitement le nom du service (par exemple
articles: Model\ArticleRepository(@mainDb)). Il est cependant plus commode soit de désactiver l'autowiring pour l'un des services, soit de préférer un service aux autres.
Désactiver l'autowiring
Nous pouvons désactiver l'autowiring d'un service à l'aide de l'option autowired: false :
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb:
create: PDO('sqlite::memory:')
autowired: false # le service tempDb est exclu de l'autowiring
articles: Model\ArticleRepository # passe donc mainDb au constructeur
Le service articles ne lèvera pas d'exception au sujet de deux services PDO correspondants
(mainDb et tempDb) disponibles pour le constructeur, car il ne prend en compte que le service
mainDb.
L'autowiring peut aussi être désactivé globalement pour des types entiers à l'aide de l'option de configuration di › excluded, qui énumère les
types (et leurs descendants) qui ne doivent jamais être autowirés.
La configuration de l'autowiring dans Nette diffère de celle de Symfony. Dans Symfony,
autowire: false signifie que l'autowiring ne doit pas être utilisé pour les arguments du constructeur du service.
Dans Nette, l'autowiring s'applique aux arguments du constructeur et à toute autre méthode appelée par le conteneur (comme
l'injection par setter). L'option autowired: false empêche le conteneur de passer automatiquement cette instance de
service comme dépendance à d'autres services.
Préférence d'autowiring
Si nous avons plusieurs services du même type et que nous indiquons l'option autowired pour l'un d'eux, ce
service devient le service préféré :
services:
mainDb:
create: PDO(%dsn%, %user%, %password%)
autowired: PDO # devient le préféré
tempDb:
create: PDO('sqlite::memory:')
articles: Model\ArticleRepository
Le service articles ne lèvera pas d'exception au sujet de plusieurs services PDO correspondants
(mainDb et tempDb), mais utilisera le service préféré, à savoir mainDb.
Collection de services
L'autowiring sait aussi passer des tableaux de services d'un type donné. Comme PHP ne permet pas nativement d'indiquer le type
des éléments d'un tableau dans les déclarations de type, vous devez compléter la déclaration array par un
commentaire phpDoc précisant le type des éléments, par exemple ClassName[] :
namespace Model;
class ShipManager
{
/**
* @param Shipper[] $shippers
*/
public function __construct(array $shippers)
{}
}
Le conteneur DI passe alors automatiquement un tableau des services correspondant au type donné. Il omet les services dont l'autowiring est désactivé et n'inclut jamais dans sa propre collection le service en cours de création. Contrairement au passage d'un service isolé, restreindre l'autowiring à un type donné ou marquer un service comme préféré n'a ici aucun effet – le tableau contient toujours tous les services du type donné.
Le type indiqué dans le commentaire peut aussi prendre la forme array<int, Class> ou
list<Class>. Si vous ne maîtrisez pas la forme du commentaire phpDoc, vous pouvez passer un tableau de
services directement dans la configuration à l'aide de typed().
Arguments scalaires
L'autowiring ne fonctionne que pour les objets et les tableaux d'objets. Les arguments scalaires (par exemple les chaînes, les nombres, les booléens) doivent être indiqués dans la configuration. Une alternative consiste à créer un objet de configuration qui encapsule la valeur scalaire (ou plusieurs valeurs). Cet objet peut ensuite être passé par autowiring.
class MySettings
{
public function __construct(
// readonly peut être utilisé depuis PHP 8.1
public readonly bool $value,
)
{}
}
Vous l'enregistrez comme service en l'ajoutant à la configuration :
services:
- MySettings('any value')
Les autres classes peuvent ensuite le demander par autowiring.
Dépendances facultatives
Si un paramètre de constructeur ou de méthode a une valeur par défaut et qu'aucun service du type requis n'existe dans le conteneur, l'autowiring ne lève pas d'exception – il saute simplement l'argument, si bien que la valeur par défaut est utilisée. C'est ainsi que vous déclarez des dépendances facultatives :
class Foo
{
public function __construct(
private ?Logger $logger = null,
) {}
}
En revanche, pour un paramètre sans valeur par défaut, un service manquant provoque toujours une exception.
Restreindre l'autowiring
Pour chaque service, l'autowiring peut être restreint à certaines classes ou interfaces.
Normalement, l'autowiring passe un service à tout paramètre de méthode dont le type correspond au service. Restreindre signifie que nous posons des conditions que les types indiqués pour les paramètres des méthodes doivent remplir pour que le service leur soit passé.
Prenons 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, parent et child correspondent
childDep: ChildDependent # l'autowiring passe le service child au constructeur
Le service parentDep lève l'exception Multiple services of type ParentClass found: child, parent,
car les services parent et child conviennent tous deux à son constructeur et l'autowiring ne peut pas
décider lequel choisir.
Pour le service child, nous pouvons donc restreindre 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
Désormais, c'est le service parent qui est passé au constructeur du service parentDep, car il est
maintenant le seul objet correspondant. Le service child n'y est plus passé par l'autowiring. Oui, le service
child est toujours du type ParentClass, mais la condition de restriction
autowired: ChildClass fait qu'il ne sera passé qu'aux paramètres explicitement typés ChildClass (ou
ses sous-types). Comme ParentDependent exige ParentClass, le service child n'y est plus
considéré comme candidat à l'autowiring.
Pour le service child, autowired: ChildClass pourrait aussi s'écrire autowired: self,
car self est un placeholder pour la classe du service courant.
Dans la clé autowired, il est également possible d'indiquer plusieurs classes ou interfaces sous forme de
tableau :
autowired: [ParentClass, FooInterface]
Essayons d'ajouter des interfaces à l'exemple :
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 restreignons en rien le service child, il conviendra aux constructeurs de toutes les classes
FooDependent, BarDependent, ParentDependent et ChildDependent, et l'autowiring
l'y passera.
En revanche, si nous restreignons son autowiring à ChildClass avec autowired: ChildClass (ou
self), l'autowiring ne le passera qu'au constructeur de ChildDependent, car celui-ci exige un argument
de type ChildClass et il est vrai que ChildClass est du type ChildClass. Aucun des
types requis par les autres paramètres n'est ChildClass ni un de ses sous-types, le service ne leur est donc
pas passé.
Si nous le restreignons à ParentClass avec autowired: ParentClass, l'autowiring le passera de
nouveau au constructeur de ChildDependent (car le ChildClass requis est un sous-type de
ParentClass) et désormais aussi au constructeur de ParentDependent, car le type requis
ParentClass convient également.
Si nous le restreignons à FooInterface, il sera toujours autowiré dans ParentDependent (le
ParentClass requis est un sous-type de FooInterface) et dans ChildDependent, et en plus
dans le constructeur de FooDependent, mais pas dans BarDependent, car BarInterface n'est
pas un sous-type 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