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