Autowiring

Autowiring je odlična lastnost, ki zna samodejno posredovati v konstruktor in druge metode zahtevane storitve, tako da jih sploh ni treba pisati. Prihrani vam veliko časa.

Zahvaljujoč temu lahko izpustimo večino argumentov pri pisanju definicij storitev. Namesto:

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

Zadostuje napisati:

services:
	articles: Model\ArticleRepository

Autowiring se ravna po tipih, zato mora biti za delovanje razred ArticleRepository definiran približno takole:

namespace Model;

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

Da bi lahko uporabili autowiring, mora za vsak tip v vsebniku obstajati točno ena storitev. Če bi jih bilo več, autowiring ne bi vedel, katero naj posreduje, in bi vrgel izjemo:

services:
	mainDb: PDO(%dsn%, %user%, %password%)
	tempDb: PDO('sqlite::memory:')
	articles: Model\ArticleRepository  # VRŽE IZJEMO, ustrezata mainDb in tempDb

Rešitev bi bila bodisi obiti autowiring in eksplicitno navesti ime storitve (tj. articles: Model\ArticleRepository(@mainDb)). Pametneje pa je autowiring ene od storitev izklopiti ali prvo storitev dati prednost.

Izklop autowiringa

Autowiring storitve lahko izklopimo z uporabo možnosti autowired: no:

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

	tempDb:
		create: PDO('sqlite::memory:')
		autowired: false               # storitev tempDb je izključena iz autowiringa

	articles: Model\ArticleRepository  # zato posreduje v konstruktor mainDb

Storitev articles ne bo vrgla izjeme, da obstajata dve ustrezni storitvi tipa PDO (tj. mainDb in tempDb), ki ju je mogoče posredovati v konstruktor, ker vidi samo storitev mainDb.

Konfiguracija autowiringa v Nette deluje drugače kot v Symfonyju, kjer možnost autowire: false pove, da se autowiring ne sme uporabljati za argumente konstruktorja dane storitve. V Nette se autowiring uporablja vedno, bodisi za argumente konstruktorja ali katere koli druge metode. Možnost autowired: false pove, da instanca dane storitve ne sme biti nikamor posredovana z uporabo autowiringa.

Prednost autowiringa

Če imamo več storitev istega tipa in pri eni od njih navedemo možnost autowired, postane ta storitev prednostna:

services:
	mainDb:
		create: PDO(%dsn%, %user%, %password%)
		autowired: PDO    # postane prednostna

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

	articles: Model\ArticleRepository

Storitev articles ne bo vrgla izjeme, da obstajata dve ustrezni storitvi tipa PDO (tj. mainDb in tempDb), ampak bo uporabila prednostno storitev, torej mainDb.

Polje storitev

Autowiring zna posredovati tudi polja storitev določenega tipa. Ker v PHP ni mogoče nativno zapisati tipa elementov polja, je treba poleg tipa array dopolniti tudi phpDoc komentar s tipom elementa v obliki ClassName[]:

namespace Model;

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

DI vsebnik nato samodejno posreduje polje storitev, ki ustrezajo danemu tipu. Izpusti storitve, ki imajo izklopljen autowiring.

Tip v komentarju je lahko tudi v obliki array<int, Class> ali list<Class>. Če ne morete vplivati na obliko phpDoc komentarja, lahko polje storitev posredujete neposredno v konfiguraciji z uporabo typed().

Skalarni argumenti

Autowiring zna vstavljati samo objekte in polja objektov. Skalarne argumente (npr. nize, števila, booleane) zapišemo v konfiguraciji. Alternativa je ustvariti settings-objekt, ki skalarno vrednost (ali več vrednosti) zapakira v obliko objekta, ki ga nato lahko spet posredujemo z uporabo autowiringa.

class MySettings
{
	public function __construct(
		// readonly je mogoče uporabiti od PHP 8.1
		public readonly bool $value,
	)
	{}
}

Iz njega ustvarite storitev z dodajanjem v konfiguracijo:

services:
	- MySettings('any value')

Vsi razredi jo nato zahtevajo z uporabo autowiringa.

Omejitev autowiringa

Posameznim storitvam lahko autowiring omejimo samo na določene razrede ali vmesnike.

Običajno autowiring storitev posreduje v vsak parameter metode, katerega tipu storitev ustreza. Omejitev pomeni, da določimo pogoje, ki jim morajo ustrezati tipi, navedeni pri parametrih metod, da jim bo storitev posredovana.

Poglejmo si to na primeru:

class ParentClass
{}

class ChildClass extends ParentClass
{}

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

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

Če bi jih vse registrirali kot storitve, bi autowiring spodletel:

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent  # VRŽE IZJEMO, ustrezata storitvi parent in child
	childDep: ChildDependent    # autowiring posreduje v konstruktor storitev child

Storitev parentDep vrže izjemo Multiple services of type ParentClass found: parent, child, ker v njen konstruktor ustrezata obe storitvi parent in child, in autowiring ne more odločiti, katero naj izbere.

Pri storitvi child lahko zato omejimo njen autowiring na tip ChildClass:

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass   # lahko napišemo tudi 'autowired: self'

	parentDep: ParentDependent  # autowiring posreduje v konstruktor storitev parent
	childDep: ChildDependent    # autowiring posreduje v konstruktor storitev child

Zdaj se v konstruktor storitve parentDep posreduje storitev parent, ker je zdaj to edini ustrezen objekt. Storitve child autowiring tja ne posreduje več. Da, storitev child je še vedno tipa ParentClass, vendar ne velja več omejitveni pogoj, dan za tip parametra, tj. ne velja, da je ParentClass nadtip ChildClass.

Pri storitvi child bi bilo mogoče autowired: ChildClass zapisati tudi kot autowired: self, ker je self nadomestno ime za razred trenutne storitve.

V ključu autowired je mogoče navesti tudi več razredov ali vmesnikov kot polje:

autowired: [BarClass, FooInterface]

Poskusimo primer dopolniti še z vmesniki:

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

Če storitve child nikakor ne omejimo, bo ustrezala konstruktorjem vseh razredov FooDependent, BarDependent, ParentDependent in ChildDependent, in autowiring jo bo tja posredoval.

Če pa njen autowiring omejimo na ChildClass z autowired: ChildClass (ali self), jo bo autowiring posredoval samo v konstruktor ChildDependent, ker zahteva argument tipa ChildClass in velja, da je ChildClass tipa ChildClass. Noben drug tip, naveden pri drugih parametrih, ni nadtip ChildClass, zato se storitev ne posreduje.

Če jo omejimo na ParentClass z autowired: ParentClass, jo bo autowiring spet posredoval v konstruktor ChildDependent (ker je zahtevani ChildClass nadtip ParentClass) in na novo tudi v konstruktor ParentDependent, ker je zahtevani tip ParentClass prav tako ustrezen.

Če jo omejimo na FooInterface, bo še vedno avtomatsko povezana v ParentDependent (zahtevani ParentClass je nadtip FooInterface) in ChildDependent, poleg tega pa tudi v konstruktor FooDependent, vendar ne v BarDependent, ker BarInterface ni nadtip FooInterface.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent        # autowiring posreduje v konstruktor child
	barDep: BarDependent        # VRŽE IZJEMO, nobena storitev ne ustreza
	parentDep: ParentDependent  # autowiring posreduje v konstruktor child
	childDep: ChildDependent    # autowiring posreduje v konstruktor child
različica: 3.x