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