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