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