Autowiring

Autowiring to świetna funkcja, która automatycznie przekazuje do konstruktora i innych metod potrzebne usługi, dzięki czemu nie musimy ich jawnie podawać. Oszczędza mnóstwo czasu.

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

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

wystarczy napisać:

services:
	articles: Model\ArticleRepository

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

namespace Model;

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

Autowiring nigdy nie używa nazw usług. Kieruje się wyłącznie systemem typów PHP, więc wie też, że klasa spełnia interfejsy, które implementuje, i klasy, po których dziedziczy. Dzięki temu nazwa usługi jest tylko pomocniczym identyfikatorem, a zmiana jej nazwy niczego w aplikacji nie zepsuje.

Aby móc używać autowiringu, w kontenerze musi istnieć dokładnie jedna usługa każdego typu. Gdyby było ich więcej, autowiring nie wiedziałby, którą przekazać, i zgłosiłby wyjątek:

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

Jednym z rozwiązań jest obejście autowiringu i jawne podanie nazwy usługi (np. articles: Model\ArticleRepository(@mainDb)). Wygodniejszym podejściem jest jednak albo wyłączenie autowiringu dla jednej z usług, albo wskazanie jednej usługi jako preferowanej.

Wyłączenie autowiringu

Autowiring dla usługi możemy wyłączyć opcją autowired: false:

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

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

	articles: Model\ArticleRepository  # dlatego do konstruktora trafia mainDb

Usługa articles nie zgłosi wyjątku o dwóch pasujących usługach PDO (mainDb i tempDb) dostępnych dla konstruktora, bo bierze pod uwagę tylko usługę mainDb.

Autowiring można też wyłączyć globalnie dla całych typów, opcją konfiguracyjną di › excluded, która wymienia typy (i ich potomków), które nigdy nie mają być autowirowane.

Konfiguracja autowiringu w Nette różni się od Symfony. W Symfony autowire: false oznacza, że autowiring nie ma być używany dla argumentów konstruktora usługi. W Nette autowiring dotyczy argumentów konstruktora i wszelkich innych metod wywoływanych przez kontener (jak setter injection). Opcja autowired: false uniemożliwia kontenerowi automatyczne przekazywanie tej instancji usługi jako zależności do innych usług.

Preferowanie w autowiringu

Jeśli mamy kilka usług tego samego typu i dla jednej z nich podamy opcję autowired, usługa ta staje się preferowana:

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

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

	articles: Model\ArticleRepository

Usługa articles nie zgłosi wyjątku o kilku pasujących usługach PDO (mainDb i tempDb), lecz użyje preferowanej, czyli mainDb.

Kolekcja usług

Autowiring potrafi przekazywać również tablice usług określonego typu. Ponieważ PHP natywnie nie obsługuje podawania typu elementów tablicy w deklaracjach typów, musisz uzupełnić deklarację typu array komentarzem phpDoc podającym typ elementu, w rodzaju ClassName[]:

namespace Model;

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

Kontener DI przekazuje wtedy automatycznie tablicę usług odpowiadających podanemu typowi. Pomija usługi z wyłączonym autowiringiem i nigdy nie umieszcza aktualnie tworzonej usługi w jej własnej kolekcji. W odróżnieniu od przekazywania pojedynczej usługi zawężenie autowiringu do konkretnego typu albo oznaczenie usługi jako preferowanej nie ma tu żadnego znaczenia – tablica zawiera zawsze wszystkie usługi danego typu.

Typ w komentarzu może mieć również postać array<int, Class> albo list<Class>. Jeśli nie masz kontroli nad postacią komentarza phpDoc, możesz przekazać tablicę usług bezpośrednio w konfiguracji za pomocą typed().

Argumenty skalarne

Autowiring działa tylko dla obiektów i tablic obiektów. Argumenty skalarne (np. stringi, liczby, wartości logiczne) trzeba podać w konfiguracji. Alternatywą jest utworzenie obiektu ustawień, który zamyka wartość skalarną (albo wiele wartości). Taki obiekt można potem przekazywać autowiringiem.

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

Rejestrujesz go jako usługę, dodając do konfiguracji:

services:
	- MySettings('any value')

Inne klasy mogą go potem otrzymać przez autowiring.

Zależności opcjonalne

Jeśli parametr konstruktora albo metody ma wartość domyślną, a w kontenerze nie ma usługi wymaganego typu, autowiring nie zgłasza wyjątku – po prostu pomija argument, więc użyta zostaje wartość domyślna. Tak deklaruje się zależności opcjonalne:

class Foo
{
	public function __construct(
		private ?Logger $logger = null,
	) {}
}

Odwrotnie, przy parametrze bez wartości domyślnej brak usługi zawsze powoduje wyjątek.

Zawężanie autowiringu

Dla poszczególnych usług autowiring można zawęzić do konkretnych klas albo interfejsów.

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

Weźmy przykład:

class ParentClass
{}

class ChildClass extends ParentClass
{}

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

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

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

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent  # ZGŁASZA WYJĄTEK, pasują zarówno usługa parent, jak i child
	childDep: ChildDependent    # autowiring przekazuje do konstruktora usługę child

Usługa parentDep zgłasza wyjątek Multiple services of type ParentClass found: child, parent, bo do jej konstruktora pasują zarówno usługa parent, jak i child, a autowiring nie potrafi zdecydować, którą wybrać.

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

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass   # można też zapisać jako 'autowired: self'

	parentDep: ParentDependent  # autowiring przekazuje do konstruktora usługę parent
	childDep: ChildDependent    # autowiring przekazuje do konstruktora usługę child

Teraz do konstruktora usługi parentDep przekazywana jest usługa parent, bo jest jedynym pasującym obiektem. Usługa child nie jest już tam przekazywana przez autowiring. Tak, usługa child nadal jest typu ParentClass, ale warunek zawężający autowired: ChildClass oznacza, że zostanie przekazana tylko do parametrów jawnie otypowanych jako ChildClass (albo jej podtypy). Ponieważ ParentDependent wymaga ParentClass, usługa child nie jest już tam brana pod uwagę jako kandydat do autowiringu.

Dla usługi child autowired: ChildClass można też zapisać jako autowired: self, bo self jest symbolem zastępczym klasy bieżącej usługi.

W kluczu autowired można też podać kilka klas albo interfejsów jako tablicę:

autowired: [ParentClass, FooInterface]

Spróbujmy dodać do przykładu 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)
	{}
}

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

Jeśli jednak zawęzimy jej autowiring do ChildClass przez autowired: ChildClass (albo self), autowiring przekaże ją tylko do konstruktora ChildDependent, bo wymaga on argumentu typu ChildClass, a zachodzi, że ChildClass jest typu ChildClass. Żaden z pozostałych wymaganych typów parametrów nie jest ChildClass ani jej podtypem, więc usługa nie jest do nich przekazywana.

Jeśli ograniczymy ją do ParentClass przez autowired: ParentClass, autowiring znów przekaże ją do konstruktora ChildDependent (bo wymagany ChildClass jest podtypem ParentClass), a teraz również do konstruktora ParentDependent, bo wymagany typ ParentClass również pasuje.

Jeśli ograniczymy ją do FooInterface, nadal będzie autowirowana do ParentDependent (wymagany ParentClass jest podtypem FooInterface) i ChildDependent, a dodatkowo do konstruktora FooDependent, ale nie do BarDependent, bo BarInterface nie jest podtypem FooInterface.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent        # autowiring przekazuje do konstruktora usługę child
	barDep: BarDependent        # ZGŁASZA WYJĄTEK, żadna usługa nie pasuje
	parentDep: ParentDependent  # autowiring przekazuje do konstruktora usługę child
	childDep: ChildDependent    # autowiring przekazuje do konstruktora usługę child
wersja: 3.x