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