Autowiring
Autowiring to świetna funkcja, która może automatycznie przekazywać wymagane usługi do konstruktora i innych metod, więc nie musimy ich w ogóle pisać. Dzięki temu można zaoszczędzić sporo czasu.
Dzięki temu możemy pominąć zdecydowaną większość argumentów przy pisaniu definicji usług. Zamiast:
services:
articles: Model\ArticleRepository(@database, @cache.storage)
Wystarczy napisać:
services:
articles: Model\ArticleRepository
Autowiring jest oparty na typie, więc aby działał, klasa ArticleRepository
musi być zdefiniowana w ten
sposób:
namespace Model;
class ArticleRepository
{
public function __construct(\PDO $db, \Nette\Caching\Storage $storage)
{}
}
Aby użyć autowiring, w kontenerze musi być tylko jedna usługa dla każdego typu. Gdyby było ich więcej niż jeden, autowiring nie wiedziałby, który z nich przekazać i rzuciłby wyjątek:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb: PDO('sqlite::memory:')
articles: Model\ArticleRepository # VYHODÍ VÝJIMKU, vyhovuje mainDb i tempDb.
Rozwiązaniem byłoby albo ominięcie autowiring i jawne podanie nazwy usługi (np.
articles: Model\ArticleRepository(@mainDb)
). Łatwiej jest jednak wyłączyć
autowiring dla jednej z usług lub nadać priorytet pierwszej usłudze.
Wyłączanie automatycznego okablowania
Można wyłączyć funkcję autopowrotu dla usługi, wybierając adres autowired: no
:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb:
create: PDO('sqlite::memory:')
autowired: false # usługa tempDb jest wyłączona z autowiring
articles: Model\ArticleRepository # dlatego przechodzi do konstruktora mainDb
Serwis articles
nie rzuca wyjątku, że istnieją dwa pasujące serwisy typu PDO
(czyli
mainDb
i tempDb
), które można przekazać do konstruktora, ponieważ widzi tylko serwis
mainDb
.
Konfiguracja autowiring w Nette działa inaczej niż w Symfony, gdzie opcja autowire: false
mówi,
aby nie używać autowiring dla argumentów do konstruktora danego serwisu. W Nette, autowiring jest zawsze używany, 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ą autowiring.
Preferencje dotyczące automatycznego okablowania
Jeśli masz wiele usług tego samego typu i określisz autowired
dla jednej z nich, ta usługa staje się
usługą preferowaną:
services:
mainDb:
create: PDO(%dsn%, %user%, %password%)
autowired: PDO # stává se preferovanou
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 preferowanej usługi, mainDb
.
Pole serwisowe
Autowiring może również przekazać pole serwisowe określonego typu. Ponieważ PHP nie potrafi natywnie zapisywać typu
elementów tablicy, oprócz typu array
należy dodać komentarz phpDoc z typem elementu
ClassName[]
:
namespace Model;
class ShipManager
{
/**
* @param Shipper[] $shippers
*/
public function __construct(array $shippers)
{}
}
Następnie kontener DI automatycznie przekaże tablicę usług pasujących do tego typu. Pominie usługi, które mają wyłączone autowiring.
Typ w komentarzu może mieć również postać array<int, Class>
lub list<Class>
. Jeśli
nie możesz kontrolować formy komentarza phpDoc, możesz przekazać tablicę usług bezpośrednio w konfiguracji za pomocą typed()
.
Argumenty skalarne
Autowiring może ustawiać tylko obiekty i tablice obiektów. Argumenty skalarne (np. ciągi znaków, liczby, booleans) są zapisywane w konfiguracji. Alternatywą jest stworzenie settings-object, który enkapsuluje wartość skalarną (lub wiele wartości) jako obiekt, który następnie można przekazać ponownie za pomocą autowiring.
class MySettings
{
public function __construct(
// readonly może być używany od PHP 8.1
public readonly bool $value,
)
{}
}
Tworzysz z niego usługę, dodając ją do konfiguracji:
services:
- MySettings('any value')
Wszystkie klasy będą wtedy żądać go poprzez autowiring.
Zawężenie autowiringów
Możesz ograniczyć autowiring dla poszczególnych usług do określonych klas lub interfejsów.
Normalnie autowiring przekazuje usługę do każdego parametru metody, której typowi odpowiada usługa. Zawężanie oznacza, że określamy warunki, jakie muszą spełniać typy określone dla parametrów metody, aby można było przekazać do nich usługę.
Pokażmy to na przykładzie:
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 się nie powiódł:
services:
parent: ParentClass
child: ChildClass
parentDep: ParentDependent # WYJĄTEK WYJĄTEK, zarówno usługi rodzica jak i dziecka są zgodne
childDep: ChildDependent # autowiring przekazuje usługę dziecka do konstruktora
Serwis parentDep
rzuciłby wyjątek Multiple services of type ParentClass found: parent, child
,
ponieważ zarówno parent
, jak i child
mieszczą się w jego kontenerze, a autowiring nie może
zdecydować, który z nich wybrać.
Dla serwisu child
możemy zatem zawęzić jego autowizerunki do typu ChildClass
:
services:
parent: ParentClass
child:
create: ChildClass
autowired: ChildClass # można również napisać 'autowired: self'
parentDep: ParentDependent # autowiring przekazuje usługę nadrzędną do konstruktora
childDep: ChildDependent # autowiring przekazuje usługę dziecka do konstruktora
Teraz serwis parent
jest przekazywany do konstruktora serwisu parentDep
, ponieważ jest to teraz
jedyny pasujący obiekt. Usługa child
nie będzie już tam autowired. Tak, usługa child
jest nadal
typu ParentClass
, ale warunek zawężający podany dla parametru typu nie ma już zastosowania, tzn. nie ma już
racji, że ParentClass
jest supertypem ChildClass
.
W przypadku serwisu child
, autowired: ChildClass
może być również zapisany jako
autowired: self
, ponieważ self
jest miejscem dla aktualnej klasy serwisu.
Możliwe jest również wypisanie wielu klas lub interfejsów jako tablic w kluczu autowired
:
autowired: [BarClass, FooInterface]
Spróbujmy dodać do przykładu interfejs:
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 nie ograniczymy w żaden sposób serwisu child
, to zmieści się on w konstruktorach wszystkich klas
FooDependent
, BarDependent
, ParentDependent
i ChildDependent
i autowiring go
tam przekaże.
Jeśli jednak ograniczymy jego autowiring do ChildClass
za pomocą autowired: ChildClass
(lub
self
), autowiring przekaże go tylko do konstruktora ChildDependent
, ponieważ wymaga on argumentu typu
ChildClass
i prawdą jest, że ChildClass
jest typu ChildClass
. Żaden inny typ
określony dla pozostałych parametrów nie jest supersetem ChildClass
, więc serwis nie zostanie przekazany.
Jeśli ograniczymy go do ParentClass
za pomocą autowired: ParentClass
, autowiring przekaże go
ponownie do konstruktora ChildDependent
(ponieważ wymagany ChildClass
jest supersetem
ParentClass
, a teraz do konstruktora ParentDependent
, ponieważ wymagany typ ParentClass
jest również dopasowany.
Jeśli ograniczymy go do FooInterface
, to nadal będzie autowire do ParentDependent
(wymagany
ParentClass
jest nadtypem FooInterface
) i ChildDependent
, ale dodatkowo do konstruktora
FooDependent
, ale nie do BarDependent
, ponieważ BarInterface
nie jest nadtypem
FooInterface
.
services:
child:
create: ChildClass
autowired: FooInterface
fooDep: FooDependent # autowiring przechodzi do konstruktora dziecka
barDep: BarDependent # rzuca wyjątek, żadna usługa nie pasuje
parentDep: ParentDependent # autowiring przechodzi do konstruktora dziecka
childDep: ChildDependent # autowiring przechodzi do konstruktora dziecka