Autowiring

Autowiring to świetna funkcja, która potrafi automatycznie przekazywać do konstruktora i innych metod wymagane usługi, dzięki czemu nie musimy ich w ogóle pisać. Oszczędza to mnóstwo czasu.

Dzięki temu możemy pominąć zdecydowaną większość argumentów podczas pisania definicji usług. Zamiast:

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

Wystarczy napisać:

services:
	articles: Model\ArticleRepository

Autowiring kieruje się typami, więc aby działał, klasa ArticleRepository musi być zdefiniowana mniej więcej tak:

namespace Model;

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

Aby można było użyć autowiringu, dla każdego typu musi istnieć w kontenerze dokładnie jedna usługa. Jeśli byłoby ich więcej, autowiring nie wiedziałby, którą z nich przekazać i rzuciłby wyjątek:

services:
	mainDb: PDO(%dsn%, %user%, %password%)
	tempDb: PDO('sqlite::memory:')
	articles: Model\ArticleRepository  # RZUCI WYJĄTEK, pasuje zarówno mainDb, jak i tempDb

Rozwiązaniem byłoby albo obejście autowiringu i jawne podanie nazwy usługi (tj. articles: Model\ArticleRepository(@mainDb)). Lepszym rozwiązaniem jest jednak wyłączenie autowiringu dla jednej z usług lub nadanie priorytetu pierwszej usłudze.

Wyłączenie autowiringu

Autowiring usługi możemy wyłączyć za pomocą opcji autowired: no:

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

	tempDb:
		create: PDO('sqlite::memory:')
		autowired: false               # usługa tempDb jest wyłączona z autowiringu

	articles: Model\ArticleRepository  # w związku z tym przekazuje do konstruktora mainDb

Usługa articles nie rzuci wyjątku, że istnieją dwie pasujące usługi typu PDO (tj. mainDb i tempDb), które można przekazać do konstruktora, ponieważ widzi tylko usługę mainDb.

Konfiguracja autowiringu w Nette działa inaczej niż w Symfony, gdzie opcja autowire: false mówi, że nie należy używać autowiringu dla argumentów konstruktora danej usługi. W Nette autowiring jest używany zawsze, czy to dla argumentów konstruktora, czy jakiejkolwiek innej metody. Opcja autowired: false mówi, że instancja danej usługi nie powinna być nigdzie przekazywana za pomocą autowiringu.

Preferencja autowiringu

Jeśli mamy więcej usług tego samego typu i dla jednej z nich podamy opcję autowired, staje się ona usługą preferowaną:

services:
	mainDb:
		create: PDO(%dsn%, %user%, %password%)
		autowired: PDO    # staje się preferowaną

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

	articles: Model\ArticleRepository

Usługa articles nie rzuci wyjątku, że istnieją dwie pasujące usługi typu PDO (tj. mainDb i tempDb), ale użyje usługi preferowanej, czyli mainDb.

Tablica usług

Autowiring potrafi przekazywać również tablice usług określonego typu. Ponieważ w PHP nie można natywnie zapisać typu elementów tablicy, oprócz typu array należy dodać komentarz phpDoc z typem elementu w formacie ClassName[]:

namespace Model;

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

Kontener DI następnie automatycznie przekaże tablicę usług odpowiadających danemu typowi. Pominie usługi, które mają wyłączony autowiring.

Typ w komentarzu może być również w formacie array<int, Class> lub list<Class>. Jeśli nie możesz wpłynąć na postać komentarza phpDoc, możesz przekazać tablicę usług bezpośrednio w konfiguracji za pomocą typed().

Argumenty skalarne

Autowiring potrafi podstawiać tylko obiekty i tablice obiektów. Argumenty skalarne (np. ciągi znaków, liczby, wartości logiczne) zapisujemy w konfiguracji. Alternatywą jest utworzenie obiektu ustawień, który enkapsuluje wartość skalarną (lub więcej wartości) w postaci obiektu, a ten następnie można ponownie przekazywać za pomocą autowiringu.

class MySettings
{
	public function __construct(
		// readonly można używać od PHP 8.1
		public readonly bool $value,
	)
	{}
}

Utworzysz z niego usługę, dodając ją do konfiguracji:

services:
	- MySettings('any value')

Wszystkie klasy następnie zażądają jej za pomocą autowiringu.

Zawężenie autowiringu

Dla poszczególnych usług można zawęzić autowiring tylko do określonych klas lub interfejsów.

Normalnie autowiring przekazuje usługę do każdego parametru metody, którego typowi usługa odpowiada. Zawężenie oznacza, że ustalamy warunki, które muszą spełniać typy podane przy parametrach metod, aby usługa została im przekazana.

Pokażemy to na przykładzie:

class ParentClass
{}

class ChildClass extends ParentClass
{}

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

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

Gdybyśmy wszystkie zarejestrowali jako usługi, autowiring by zawiódł:

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent  # RZUCI WYJĄTEK, pasują usługi parent i child
	childDep: ChildDependent    # autowiring przekaże do konstruktora usługę child

Usługa parentDep rzuci wyjątek Multiple services of type ParentClass found: parent, child, ponieważ do jej konstruktora pasują obie usługi parent i child, a autowiring nie może zdecydować, którą z nich wybrać.

Dla usługi child możemy zatem zawęzić jej autowiring do typu ChildClass:

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass   # można napisać również 'autowired: self'

	parentDep: ParentDependent  # autowiring przekaże do konstruktora usługę parent
	childDep: ChildDependent    # autowiring przekaże do konstruktora usługę child

Teraz do konstruktora usługi parentDep zostanie przekazana usługa parent, ponieważ jest to teraz jedyny pasujący obiekt. Usługi child autowiring już tam nie przekaże. Tak, usługa child nadal jest typu ParentClass, ale nie jest już spełniony warunek zawężający podany dla typu parametru, tj. nie jest prawdą, że ParentClass jest nadtypem ChildClass.

Dla usługi child można by autowired: ChildClass zapisać również jako autowired: self, ponieważ self jest zastępczym oznaczeniem dla klasy bieżącej usługi.

W kluczu autowired można podać również kilka klas lub interfejsów jako tablicę:

autowired: [BarClass, FooInterface]

Spróbujmy uzupełnić przykład o interfejsy:

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

Gdy usługi child w żaden sposób nie ograniczymy, będzie pasować do konstruktorów wszystkich klas FooDependent, BarDependent, ParentDependent i ChildDependent, a autowiring ją tam przekaże.

Jeśli jednak jej autowiring zawęzimy do ChildClass za pomocą autowired: ChildClass (lub self), autowiring przekaże ją tylko do konstruktora ChildDependent, ponieważ wymaga on argumentu typu ChildClass i jest prawdą, że ChildClass jest typu ChildClass. Żaden inny typ podany przy pozostałych parametrach nie jest nadtypem ChildClass, więc usługa nie zostanie przekazana.

Jeśli ograniczymy ją do ParentClass za pomocą autowired: ParentClass, autowiring przekaże ją ponownie do konstruktora ChildDependent (ponieważ wymagany ChildClass jest nadtypem ParentClass), a nowo również do konstruktora ParentDependent, ponieważ wymagany typ ParentClass jest również pasujący.

Jeśli ograniczymy ją do FooInterface, nadal będzie autowirowana do ParentDependent (wymagany ParentClass jest nadtypem FooInterface) i ChildDependent, ale dodatkowo również do konstruktora FooDependent, jednak nie do BarDependent, ponieważ BarInterface nie jest nadtypem FooInterface.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent        # autowiring przekaże do konstruktora child
	barDep: BarDependent        # RZUCI WYJĄTEK, żadna usługa nie pasuje
	parentDep: ParentDependent  # autowiring przekaże do konstruktora child
	childDep: ChildDependent    # autowiring przekaże do konstruktora child