Autowiring

Autowiring ist eine großartige Funktion, die automatisch die benötigten Dienste an den Konstruktor und andere Methoden übergeben kann, sodass wir sie überhaupt nicht schreiben müssen. Es spart Ihnen viel Zeit.

Dadurch können wir die meisten Argumente beim Schreiben von Dienstdefinitionen weglassen. Anstelle von:

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

Reicht es aus zu schreiben:

services:
	articles: Model\ArticleRepository

Autowiring orientiert sich an Typen, daher muss die Klasse ArticleRepository ungefähr so definiert sein, damit es funktioniert:

namespace Model;

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

Um Autowiring verwenden zu können, muss für jeden Typ im Container genau ein Dienst vorhanden sein. Gäbe es mehr, wüsste Autowiring nicht, welchen er übergeben soll, und würde eine Ausnahme auslösen:

services:
	mainDb: PDO(%dsn%, %user%, %password%)
	tempDb: PDO('sqlite::memory:')
	articles: Model\ArticleRepository  # WIRFT EINE AUSNAHME, sowohl mainDb als auch tempDb passen

Die Lösung wäre entweder, Autowiring zu umgehen und den Dienstnamen explizit anzugeben (d.h. articles: Model\ArticleRepository(@mainDb)). Geschickter ist es jedoch, das Autowiring für einen der Dienste zu deaktivieren oder den ersten Dienst zu bevorzugen.

Deaktivieren des Autowirings

Wir können das Autowiring eines Dienstes mit der Option autowired: no deaktivieren:

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

	tempDb:
		create: PDO('sqlite::memory:')
		autowired: false               # Der Dienst tempDb wird vom Autowiring ausgeschlossen

	articles: Model\ArticleRepository  # übergibt daher mainDb an den Konstruktor

Der Dienst articles löst keine Ausnahme aus, dass zwei passende Dienste vom Typ PDO (d.h. mainDb und tempDb) existieren, die an den Konstruktor übergeben werden können, da er nur den Dienst mainDb sieht.

Die Konfiguration des Autowirings in Nette funktioniert anders als in Symfony, wo die Option autowire: false besagt, dass Autowiring nicht für die Konstruktorargumente des betreffenden Dienstes verwendet werden soll. In Nette wird Autowiring immer verwendet, sei es für Konstruktorargumente oder für andere Methoden. Die Option autowired: false besagt, dass die Instanz des betreffenden Dienstes nirgendwo per Autowiring übergeben werden soll.

Bevorzugung beim Autowiring

Wenn wir mehrere Dienste desselben Typs haben und bei einem davon die Option autowired angeben, wird dieser Dienst bevorzugt:

services:
	mainDb:
		create: PDO(%dsn%, %user%, %password%)
		autowired: PDO    # wird bevorzugt

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

	articles: Model\ArticleRepository

Der Dienst articles löst keine Ausnahme aus, dass zwei passende Dienste vom Typ PDO (d.h. mainDb und tempDb) existieren, sondern verwendet den bevorzugten Dienst, also mainDb.

Array von Diensten

Autowiring kann auch Arrays von Diensten eines bestimmten Typs übergeben. Da in PHP der Typ der Array-Elemente nicht nativ angegeben werden kann, muss zusätzlich zum Typ array ein phpDoc-Kommentar mit dem Elementtyp im Format ClassName[] hinzugefügt werden:

namespace Model;

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

Der DI-Container übergibt dann automatisch ein Array von Diensten, die dem angegebenen Typ entsprechen. Dienste, deren Autowiring deaktiviert ist, werden ausgelassen.

Der Typ im Kommentar kann auch im Format array<int, Class> oder list<Class> vorliegen. Wenn Sie die Form des phpDoc-Kommentars nicht beeinflussen können, können Sie das Array von Diensten direkt in der Konfiguration mithilfe von typed() übergeben.

Skalare Argumente

Autowiring kann nur Objekte und Arrays von Objekten einfügen. Skalare Argumente (z. B. Zeichenketten, Zahlen, Booleans) schreiben wir in der Konfiguration. Eine Alternative ist die Erstellung eines Einstellungsobjekts, das den skalaren Wert (oder mehrere Werte) in ein Objekt kapselt, welches dann wieder per Autowiring übergeben werden kann.

class MySettings
{
	public function __construct(
		// readonly kann ab PHP 8.1 verwendet werden
		public readonly bool $value,
	)
	{}
}

Sie erstellen daraus einen Dienst, indem Sie ihn zur Konfiguration hinzufügen:

services:
	- MySettings('any value')

Alle Klassen fordern ihn dann per Autowiring an.

Einschränken des Autowirings

Für einzelne Dienste kann das Autowiring auf bestimmte Klassen oder Schnittstellen eingeschränkt werden.

Normalerweise übergibt Autowiring einen Dienst an jeden Methodenparameter, dessen Typ dem Dienst entspricht. Die Einschränkung bedeutet, dass wir Bedingungen festlegen, denen die bei den Methodenparametern angegebenen Typen entsprechen müssen, damit der Dienst an sie übergeben wird.

Zeigen wir dies an einem Beispiel:

class ParentClass
{}

class ChildClass extends ParentClass
{}

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

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

Wenn wir sie alle als Dienste registrieren würden, würde Autowiring fehlschlagen:

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent  # WIRFT EINE AUSNAHME, sowohl parent als auch child passen
	childDep: ChildDependent    # Autowiring übergibt den Dienst child an den Konstruktor

Der Dienst parentDep löst die Ausnahme Multiple services of type ParentClass found: parent, child aus, da beide Dienste parent und child in seinen Konstruktor passen und Autowiring nicht entscheiden kann, welchen es wählen soll.

Für den Dienst child können wir daher sein Autowiring auf den Typ ChildClass einschränken:

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass   # kann auch 'autowired: self' geschrieben werden

	parentDep: ParentDependent  # Autowiring übergibt den Dienst parent an den Konstruktor
	childDep: ChildDependent    # Autowiring übergibt den Dienst child an den Konstruktor

Nun wird der Dienst parent an den Konstruktor von parentDep übergeben, da er jetzt das einzige passende Objekt ist. Der Dienst child wird dort vom Autowiring nicht mehr übergeben. Ja, der Dienst child ist immer noch vom Typ ParentClass, aber die einschränkende Bedingung für den Parametertyp gilt nicht mehr, d.h. es gilt nicht, dass ParentClass ein Supertyp von ChildClass ist.

Für den Dienst child könnte autowired: ChildClass auch als autowired: self geschrieben werden, da self ein Platzhalter für die Klasse des aktuellen Dienstes ist.

Im Schlüssel autowired können auch mehrere Klassen oder Schnittstellen als Array angegeben werden:

autowired: [BarClass, FooInterface]

Ergänzen wir das Beispiel noch um Schnittstellen:

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

Wenn wir den Dienst child nicht einschränken, passt er in die Konstruktoren aller Klassen FooDependent, BarDependent, ParentDependent und ChildDependent, und Autowiring übergibt ihn dorthin.

Wenn wir sein Autowiring jedoch auf ChildClass mit autowired: ChildClass (oder self) einschränken, übergibt Autowiring ihn nur an den Konstruktor von ChildDependent, da dieser ein Argument vom Typ ChildClass erfordert und ChildClass vom Typ ChildClass ist. Kein anderer bei den weiteren Parametern angegebener Typ ist ein Supertyp von ChildClass, daher wird der Dienst nicht übergeben.

Wenn wir ihn auf ParentClass mit autowired: ParentClass beschränken, übergibt Autowiring ihn erneut an den Konstruktor von ChildDependent (da das erforderliche ChildClass ein Supertyp von ParentClass ist) und neu auch an den Konstruktor von ParentDependent, da der erforderliche Typ ParentClass ebenfalls passend ist.

Wenn wir ihn auf FooInterface beschränken, wird er immer noch in ParentDependent (erforderliches ParentClass ist Supertyp von FooInterface) und ChildDependent autowired, aber zusätzlich auch in den Konstruktor von FooDependent, jedoch nicht in BarDependent, da BarInterface kein Supertyp von FooInterface ist.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent        # Autowiring übergibt child an den Konstruktor
	barDep: BarDependent        # WIRFT EINE AUSNAHME, kein Dienst passt
	parentDep: ParentDependent  # Autowiring übergibt child an den Konstruktor
	childDep: ChildDependent    # Autowiring übergibt child an den Konstruktor
Version: 3.x