Autowiring

Autowiring este o caracteristică excelentă care poate transmite automat serviciile necesare către constructor și alte metode, astfel încât nu trebuie să le scriem deloc. Vă economisește mult timp.

Datorită acestui fapt, putem omite marea majoritate a argumentelor atunci când scriem definiții de servicii. În loc de:

services:
	articles: Model\ArticleRepository(@database, @cache.storage)

Este suficient să scrieți:

services:
	articles: Model\ArticleRepository

Autowiring se ghidează după tipuri, așa că pentru a funcționa, clasa ArticleRepository trebuie definită aproximativ astfel:

namespace Model;

class ArticleRepository
{
	public function __construct(\PDO $db, \Nette\Caching\Storage $storage)
	{}
}

Pentru a putea utiliza autowiring, trebuie să existe exact un serviciu pentru fiecare tip în container. Dacă ar exista mai multe, autowiring nu ar ști pe care să îl transmită și ar arunca o excepție:

services:
	mainDb: PDO(%dsn%, %user%, %password%)
	tempDb: PDO('sqlite::memory:')
	articles: Model\ArticleRepository  # ARUNCĂ EXCEPȚIE, se potrivesc atât mainDb cât și tempDb

Soluția ar fi fie să ocoliți autowiring-ul și să specificați explicit numele serviciului (adică articles: Model\ArticleRepository(@mainDb)). Dar este mai convenabil să dezactivați autowiring-ul pentru unul dintre servicii sau să prioritizați primul serviciu.

Dezactivarea autowiring-ului

Putem dezactiva autowiring-ul unui serviciu folosind opțiunea autowired: no:

services:
	mainDb: PDO(%dsn%, %user%, %password%)

	tempDb:
		create: PDO('sqlite::memory:')
		autowired: false               # serviciul tempDb este exclus din autowiring

	articles: Model\ArticleRepository  # prin urmare, transmite mainDb către constructor

Serviciul articles nu aruncă o excepție că există două servicii potrivite de tip PDO (adică mainDb și tempDb) care pot fi transmise constructorului, deoarece vede doar serviciul mainDb.

Configurarea autowiring-ului în Nette funcționează diferit față de Symfony, unde opțiunea autowire: false specifică faptul că autowiring-ul nu trebuie utilizat pentru argumentele constructorului serviciului respectiv. În Nette, autowiring-ul este întotdeauna utilizat, fie pentru argumentele constructorului, fie pentru orice altă metodă. Opțiunea autowired: false specifică faptul că instanța serviciului respectiv nu trebuie transmisă nicăieri prin autowiring.

Preferința autowiring-ului

Dacă avem mai multe servicii de același tip și pentru unul dintre ele specificăm opțiunea autowired, acest serviciu devine preferat:

services:
	mainDb:
		create: PDO(%dsn%, %user%, %password%)
		autowired: PDO    # devine preferat

	tempDb:
		create: PDO('sqlite::memory:')

	articles: Model\ArticleRepository

Serviciul articles nu aruncă o excepție că există două servicii potrivite de tip PDO (adică mainDb și tempDb), ci folosește serviciul preferat, adică mainDb.

Array de servicii

Autowiring poate transmite și array-uri de servicii de un anumit tip. Deoarece în PHP nu se poate scrie nativ tipul elementelor unui array, este necesar, pe lângă tipul array, să se adauge și un comentariu phpDoc cu tipul elementului în formatul ClassName[]:

namespace Model;

class ShipManager
{
	/**
	 * @param Shipper[] $shippers
	 */
	public function __construct(array $shippers)
	{}
}

Containerul DI transmite apoi automat un array de servicii corespunzătoare tipului respectiv. Omită serviciile care au autowiring-ul dezactivat.

Tipul din comentariu poate fi și în formatul array<int, Class> sau list<Class>. Dacă nu puteți influența forma comentariului phpDoc, puteți transmite array-ul de servicii direct în configurație folosind typed().

Argumente scalare

Autowiring poate injecta doar obiecte și array-uri de obiecte. Argumentele scalare (de ex. șiruri, numere, booleeni) le scriem în configurație. O alternativă este crearea unui obiect de setări, care încapsulează valoarea scalară (sau mai multe valori) sub formă de obiect, care apoi poate fi transmis din nou prin autowiring.

class MySettings
{
	public function __construct(
		// readonly poate fi utilizat începând cu PHP 8.1
		public readonly bool $value,
	)
	{}
}

Creați un serviciu din acesta adăugându-l în configurație:

services:
	- MySettings('any value')

Toate clasele îl vor solicita apoi prin autowiring.

Restrângerea autowiring-ului

Autowiring-ul serviciilor individuale poate fi restrâns la anumite clase sau interfețe.

În mod normal, autowiring-ul transmite serviciul către fiecare parametru al metodei al cărui tip corespunde serviciului. Restrângerea înseamnă că stabilim condiții pe care tipurile specificate la parametrii metodelor trebuie să le îndeplinească pentru ca serviciul să le fie transmis.

Să ilustrăm acest lucru cu un exemplu:

class ParentClass
{}

class ChildClass extends ParentClass
{}

class ParentDependent
{
	function __construct(ParentClass $obj)
	{}
}

class ChildDependent
{
	function __construct(ChildClass $obj)
	{}
}

Dacă le-am înregistra pe toate ca servicii, autowiring-ul ar eșua:

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent  # ARUNCĂ EXCEPȚIE, se potrivesc serviciile parent și child
	childDep: ChildDependent    # autowiring transmite serviciul child către constructor

Serviciul parentDep aruncă excepția Multiple services of type ParentClass found: parent, child, deoarece ambele servicii parent și child se potrivesc constructorului său, iar autowiring-ul nu poate decide pe care să îl aleagă.

Prin urmare, pentru serviciul child, putem restrânge autowiring-ul său la tipul ChildClass:

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass   # se poate scrie și 'autowired: self'

	parentDep: ParentDependent  # autowiring transmite serviciul parent către constructor
	childDep: ChildDependent    # autowiring transmite serviciul child către constructor

Acum, serviciul parent este transmis constructorului serviciului parentDep, deoarece acum este singurul obiect potrivit. Autowiring-ul nu mai transmite serviciul child acolo. Da, serviciul child este încă de tip ParentClass, dar condiția de restrângere dată pentru tipul parametrului nu mai este valabilă, adică nu este adevărat că ParentClass este un supratip al ChildClass.

Pentru serviciul child, autowired: ChildClass ar putea fi scris și ca autowired: self, deoarece self este un substituent pentru clasa serviciului curent.

În cheia autowired este posibil să se specifice și mai multe clase sau interfețe ca un array:

autowired: [BarClass, FooInterface]

Să încercăm să completăm exemplul cu interfețe:

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)
	{}
}

Dacă nu restricționăm în niciun fel serviciul child, acesta se va potrivi constructorilor tuturor claselor FooDependent, BarDependent, ParentDependent și ChildDependent, iar autowiring-ul îl va transmite acolo.

Dar dacă îi restrângem autowiring-ul la ChildClass folosind autowired: ChildClass (sau self), autowiring-ul îl va transmite doar constructorului ChildDependent, deoarece necesită un argument de tip ChildClass și este adevărat că ChildClass este de tip ChildClass. Niciun alt tip specificat la ceilalți parametri nu este un supratip al ChildClass, deci serviciul nu este transmis.

Dacă îl restricționăm la ParentClass folosind autowired: ParentClass, autowiring-ul îl va transmite din nou constructorului ChildDependent (deoarece ChildClass necesar este un supratip al ParentClass) și, nou, și constructorului ParentDependent, deoarece tipul necesar ParentClass este, de asemenea, potrivit.

Dacă îl restricționăm la FooInterface, va fi în continuare autowired în ParentDependent (necesarul ParentClass este un supratip al FooInterface) și ChildDependent, dar în plus și în constructorul FooDependent, însă nu în BarDependent, deoarece BarInterface nu este un supratip al FooInterface.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent        # autowiring transmite child către constructor
	barDep: BarDependent        # ARUNCĂ EXCEPȚIE, niciun serviciu nu se potrivește
	parentDep: ParentDependent  # autowiring transmite child către constructor
	childDep: ChildDependent    # autowiring transmite child către constructor
versiune: 3.x