Autowiring

Az Autowiring egy nagyszerű funkció, amely automatikusan átadja a szükséges szolgáltatásokat a konstruktornak és más metódusoknak, így egyáltalán nem kell őket megírnunk. Rengeteg időt takarít meg Önnek.

Ennek köszönhetően a szolgáltatásdefiníciók írásakor a legtöbb argumentumot elhagyhatjuk. Helyette:

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

Elég ennyit írni:

services:
	articles: Model\ArticleRepository

Az autowiring típusok alapján működik, tehát ahhoz, hogy működjön, az ArticleRepository osztályt valahogy így kell definiálni:

namespace Model;

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

Az autowiring használatához minden típushoz pontosan egy szolgáltatásnak kell lennie a konténerben. Ha több lenne belőlük, az autowiring nem tudná, melyiket adja át, és kivételt dobna:

services:
	mainDb: PDO(%dsn%, %user%, %password%)
	tempDb: PDO('sqlite::memory:')
	articles: Model\ArticleRepository  # KIVÉTELT DOB, a mainDb és a tempDb is megfelel

A megoldás az lenne, ha vagy megkerülnénk az autowiringot, és explicit módon megadnánk a szolgáltatás nevét (azaz articles: Model\ArticleRepository(@mainDb)). De ügyesebb az egyik szolgáltatás autowiringját kikapcsolni, vagy az első szolgáltatást előnyben részesíteni.

Autowiring kikapcsolása

Egy szolgáltatás autowiringját kikapcsolhatjuk az autowired: no opcióval:

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

	tempDb:
		create: PDO('sqlite::memory:')
		autowired: false               # a tempDb szolgáltatás ki van zárva az autowiringból

	articles: Model\ArticleRepository  # tehát a konstruktorba a mainDb-t adja át

Az articles szolgáltatás nem dob kivételt, hogy két megfelelő PDO típusú szolgáltatás létezik (azaz mainDb és tempDb), amelyeket át lehet adni a konstruktorba, mert csak a mainDb szolgáltatást látja.

Az autowiring konfigurációja a Nette-ben másképp működik, mint a Symfony-ban, ahol az autowire: false opció azt mondja, hogy ne használja az autowiringot az adott szolgáltatás konstruktorának argumentumaihoz. A Nette-ben az autowiring mindig használatos, akár a konstruktor argumentumaihoz, akár bármely más metódushoz. Az autowired: false opció azt mondja, hogy az adott szolgáltatás példányát ne adják át sehova autowiring segítségével.

Autowiring preferencia

Ha több azonos típusú szolgáltatásunk van, és az egyiknél megadjuk az autowired opciót, ez a szolgáltatás preferálttá válik:

services:
	mainDb:
		create: PDO(%dsn%, %user%, %password%)
		autowired: PDO    # preferálttá válik

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

	articles: Model\ArticleRepository

Az articles szolgáltatás nem dob kivételt, hogy két megfelelő PDO típusú szolgáltatás létezik (azaz mainDb és tempDb), hanem a preferált szolgáltatást használja, tehát a mainDb-t.

Szolgáltatások tömbje

Az autowiring képes átadni egy adott típusú szolgáltatások tömbjét is. Mivel PHP-ban natívan nem lehet megadni a tömb elemeinek típusát, a array típus mellett egy phpDoc kommentet is hozzá kell adni az elem típusával ClassName[] formában:

namespace Model;

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

A DI konténer ezután automatikusan átadja az adott típusnak megfelelő szolgáltatások tömbjét. Kihagyja azokat a szolgáltatásokat, amelyeknek ki van kapcsolva az autowiringja.

A kommentben szereplő típus lehet array<int, Class> vagy list<Class> formájú is. Ha nem tudja befolyásolni a phpDoc komment formáját, átadhatja a szolgáltatások tömbjét közvetlenül a konfigurációban a typed() segítségével.

Skalár argumentumok

Az autowiring csak objektumokat és objektumok tömbjeit tudja beilleszteni. A skalár argumentumokat (pl. stringek, számok, logikai értékek) a konfigurációban írjuk le. Alternatíva egy settings-objektum létrehozása, amely a skalár értéket (vagy több értéket) objektum formájába csomagolja, és ezt aztán újra át lehet adni autowiring segítségével.

class MySettings
{
	public function __construct(
		// a readonly PHP 8.1-től használható
		public readonly bool $value,
	)
	{}
}

Szolgáltatást hozhat létre belőle a konfigurációhoz való hozzáadással:

services:
	- MySettings('any value')

Ezután minden osztály autowiring segítségével kérheti azt.

Autowiring szűkítése

Az egyes szolgáltatások autowiringját le lehet szűkíteni csak bizonyos osztályokra vagy interfészekre.

Normális esetben az autowiring átadja a szolgáltatást minden olyan metódusparaméternek, amelynek típusa megfelel a szolgáltatásnak. A szűkítés azt jelenti, hogy feltételeket szabunk, amelyeknek a metódusparamétereknél megadott típusoknak meg kell felelniük ahhoz, hogy a szolgáltatást átadják nekik.

Nézzünk egy példát:

class ParentClass
{}

class ChildClass extends ParentClass
{}

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

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

Ha mindegyiket szolgáltatásként regisztrálnánk, az autowiring meghiúsulna:

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent  # KIVÉTELT DOB, a parent és a child szolgáltatás is megfelel
	childDep: ChildDependent    # az autowiring a child szolgáltatást adja át a konstruktorba

A parentDep szolgáltatás Multiple services of type ParentClass found: parent, child kivételt dob, mert a konstruktorába mind a parent, mind a child szolgáltatás illeszkedik, és az autowiring nem tudja eldönteni, melyiket válassza.

Ezért a child szolgáltatásnál leszűkíthetjük az autowiringját a ChildClass típusra:

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass   # 'autowired: self'-et is lehet írni

	parentDep: ParentDependent  # az autowiring a parent szolgáltatást adja át a konstruktorba
	childDep: ChildDependent    # az autowiring a child szolgáltatást adja át a konstruktorba

Most a parentDep szolgáltatás konstruktorába a parent szolgáltatás kerül átadásra, mert most ez az egyetlen megfelelő objektum. A child szolgáltatást az autowiring már nem adja át oda. Igen, a child szolgáltatás továbbra is ParentClass típusú, de már nem teljesül a paraméter típusára vonatkozó szűkítő feltétel, azaz nem igaz, hogy a ParentClass felülírja a ChildClass-t.

A child szolgáltatásnál az autowired: ChildClass-t autowired: self-ként is lehetne írni, mivel a self az aktuális szolgáltatás osztályának helyettesítő jelölése.

Az autowired kulcsban több osztályt vagy interfészt is meg lehet adni tömbként:

autowired: [BarClass, FooInterface]

Próbáljuk meg a példát kiegészíteni egy interfésszel:

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

Ha a child szolgáltatást semmilyen módon nem korlátozzuk, akkor illeszkedni fog az összes FooDependent, BarDependent, ParentDependent és ChildDependent osztály konstruktorába, és az autowiring oda fogja átadni.

Ha azonban az autowiringját leszűkítjük a ChildClass-ra az autowired: ChildClass (vagy self) segítségével, az autowiring csak a ChildDependent konstruktorába adja át, mert az ChildClass típusú argumentumot igényel, és igaz, hogy a ChildClass típusa ChildClass. A többi paraméternél megadott további típusok egyike sem felülírja a ChildClass-t, így a szolgáltatás nem kerül átadásra.

Ha a ParentClass-ra korlátozzuk az autowired: ParentClass segítségével, az autowiring ismét átadja a ChildDependent konstruktorába (mert a szükséges ChildClass felülírja a ParentClass-t), és újonnan a ParentDependent konstruktorába is, mert a szükséges ParentClass típus szintén megfelelő.

Ha a FooInterface-re korlátozzuk, akkor továbbra is autowire-olva lesz a ParentDependent-be (a szükséges ParentClass felülírja a FooInterface-t) és a ChildDependent-be, de ráadásul a FooDependent konstruktorába is, viszont nem a BarDependent-be, mert a BarInterface nem felülírja a FooInterface-t.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent        # az autowiring a child-ot adja át a konstruktorba
	barDep: BarDependent        # KIVÉTELT DOB, egyetlen szolgáltatás sem felel meg
	parentDep: ParentDependent  # az autowiring a child-ot adja át a konstruktorba
	childDep: ChildDependent    # az autowiring a child-ot adja át a konstruktorba