Autowiring
Autowiring ist eine großartige Fähigkeit, die dem Konstruktor und anderen Methoden automatisch die benötigten Services übergibt, sodass wir sie nicht ausdrücklich angeben müssen. Das spart Ihnen viel Zeit.
Dadurch können wir beim Schreiben von Service-Definitionen die allermeisten Argumente weglassen. Statt:
services:
articles: Model\ArticleRepository(@database, @cache.storage)
schreiben Sie einfach:
services:
articles: Model\ArticleRepository
Das Autowiring richtet sich nach Typen, damit es funktioniert, muss die Klasse ArticleRepository also ungefähr so
definiert sein:
namespace Model;
class ArticleRepository
{
public function __construct(\PDO $db, \Nette\Caching\Storage $storage)
{}
}
Autowiring verwendet niemals die Namen von Services. Es richtet sich einzig nach dem Typsystem von PHP, weiß also auch, dass eine Klasse die Interfaces erfüllt, die sie implementiert, und die Klassen, von denen sie erbt. Dadurch ist der Name eines Services bloß ein Hilfsbezeichner, und ihn umzubenennen zerstört in der Anwendung nichts.
Damit sich Autowiring nutzen lässt, muss es im Container von jedem Typ genau einen Service geben. Gäbe es mehrere, wüsste das Autowiring nicht, welchen es übergeben soll, und würde eine Exception werfen:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb: PDO('sqlite::memory:')
articles: Model\ArticleRepository # WIRFT EINE EXCEPTION, sowohl mainDb als auch tempDb passen
Eine Lösung ist, das Autowiring zu umgehen und den Namen des Services ausdrücklich anzugeben (etwa
articles: Model\ArticleRepository(@mainDb)). Bequemer ist es jedoch, das Autowiring für einen der Services abzuschalten oder einen Service gegenüber den anderen zu bevorzugen.
Autowiring abschalten
Über die Option autowired: false können wir das Autowiring für einen Service abschalten:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb:
create: PDO('sqlite::memory:')
autowired: false # der Service tempDb ist vom Autowiring ausgenommen
articles: Model\ArticleRepository # dem Konstruktor wird daher mainDb übergeben
Der Service articles wirft keine Exception darüber, dass für den Konstruktor zwei passende
PDO-Services (mainDb und tempDb) zur Verfügung stehen, denn er zieht nur den Service
mainDb in Betracht.
Autowiring lässt sich über die Konfigurationsoption di › excluded auch global für ganze
Typen abschalten; dort werden die Typen (und ihre Nachfahren) aufgeführt, die nie autowiret werden sollen.
Die Konfiguration des Autowirings unterscheidet sich in Nette von der in Symfony. In Symfony bedeutet
autowire: false, dass für die Konstruktorargumente des Services kein Autowiring verwendet werden soll. In Nette
betrifft das Autowiring die Konstruktorargumente und alle weiteren Methoden, die über den Container aufgerufen werden (etwa
Setter Injection). Die Option autowired: false verhindert, dass der Container diese Instanz des Services automatisch
als Abhängigkeit an andere Services übergibt.
Bevorzugung beim Autowiring
Haben wir mehrere Services desselben Typs und geben für einen von ihnen die Option autowired an, wird dieser
Service zum bevorzugten:
services:
mainDb:
create: PDO(%dsn%, %user%, %password%)
autowired: PDO # wird zum bevorzugten
tempDb:
create: PDO('sqlite::memory:')
articles: Model\ArticleRepository
Der Service articles wirft keine Exception darüber, dass mehrere PDO-Services passen
(mainDb und tempDb), sondern verwendet den bevorzugten, also mainDb.
Sammlung von Services
Autowiring kann auch Arrays von Services eines bestimmten Typs übergeben. Weil PHP von Haus aus nicht erlaubt, den Typ der
Array-Elemente in einer Typdeklaration anzugeben, müssen Sie die Typdeklaration array um einen phpDoc-Kommentar mit
dem Typ der Elemente ergänzen, etwa ClassName[]:
namespace Model;
class ShipManager
{
/**
* @param Shipper[] $shippers
*/
public function __construct(array $shippers)
{}
}
Der DI-Container übergibt dann automatisch ein Array der Services, die dem angegebenen Typ entsprechen. Services mit abgeschaltetem Autowiring lässt er aus, und den gerade erzeugten Service nimmt er nie in dessen eigene Sammlung auf. Anders als beim Übergeben eines einzelnen Services haben das Einschränken des Autowirings auf einen bestimmten Typ und das Kennzeichnen eines Services als bevorzugt hier keine Wirkung – das Array enthält immer alle Services des angegebenen Typs.
Der Typ im Kommentar kann auch die Form array<int, Class> oder list<Class> haben. Wenn
Sie die Form des phpDoc-Kommentars nicht beeinflussen können, lässt sich ein Array von Services über typed() direkt in der
Konfiguration übergeben.
Skalare Argumente
Autowiring funktioniert nur für Objekte und Arrays von Objekten. Skalare Argumente (etwa Strings, Zahlen, boolesche Werte) müssen in der Konfiguration angegeben werden. Eine Alternative ist, ein Objekt mit Einstellungen zu erzeugen, das den skalaren Wert (oder mehrere Werte) kapselt. Dieses Objekt lässt sich dann über Autowiring übergeben.
class MySettings
{
public function __construct(
// readonly lässt sich seit PHP 8.1 verwenden
public readonly bool $value,
)
{}
}
Als Service registrieren Sie es, indem Sie es der Konfiguration hinzufügen:
services:
- MySettings('any value')
Andere Klassen können es dann über Autowiring anfordern.
Optionale Abhängigkeiten
Hat ein Parameter des Konstruktors oder einer Methode einen Standardwert und existiert im Container kein Service des verlangten Typs, wirft das Autowiring keine Exception – es überspringt das Argument einfach, sodass der Standardwert verwendet wird. So deklarieren Sie optionale Abhängigkeiten:
class Foo
{
public function __construct(
private ?Logger $logger = null,
) {}
}
Bei einem Parameter ohne Standardwert führt ein fehlender Service dagegen immer zu einer Exception.
Autowiring einschränken
Für einzelne Services lässt sich das Autowiring auf bestimmte Klassen oder Interfaces einschränken.
Normalerweise übergibt das Autowiring einen Service an jeden Methodenparameter, dessen Typ zum Service passt. Einschränken bedeutet, dass wir Bedingungen aufstellen, die die für die Methodenparameter angegebenen Typen erfüllen müssen, damit ihnen der Service übergeben wird.
Nehmen wir ein Beispiel:
class ParentClass
{}
class ChildClass extends ParentClass
{}
class ParentDependent
{
function __construct(ParentClass $obj)
{}
}
class ChildDependent
{
function __construct(ChildClass $obj)
{}
}
Würden wir sie alle als Services registrieren, scheiterte das Autowiring:
services:
parent: ParentClass
child: ChildClass
parentDep: ParentDependent # WIRFT EINE EXCEPTION, sowohl parent als auch child passen
childDep: ChildDependent # das Autowiring übergibt dem Konstruktor den Service child
Der Service parentDep wirft die Exception Multiple services of type ParentClass found: child, parent,
weil sowohl der Service parent als auch child in seinen Konstruktor passen und das Autowiring sich nicht
entscheiden kann, welchen es wählen soll.
Für den Service child können wir das Autowiring deshalb auf den Typ ChildClass einschränken:
services:
parent: ParentClass
child:
create: ChildClass
autowired: ChildClass # lässt sich auch als 'autowired: self' schreiben
parentDep: ParentDependent # das Autowiring übergibt dem Konstruktor den Service parent
childDep: ChildDependent # das Autowiring übergibt dem Konstruktor den Service child
Jetzt wird dem Konstruktor des Services parentDep der Service parent übergeben, denn er ist nun das
einzige passende Objekt. Der Service child wird vom Autowiring nicht mehr dorthin übergeben. Ja, der Service
child ist weiterhin vom Typ ParentClass, aber die Einschränkung autowired: ChildClass
bewirkt, dass er nur an Parameter übergeben wird, die ausdrücklich als ChildClass (oder als deren Untertypen)
deklariert sind. Weil ParentDependent ein ParentClass verlangt, kommt der Service child
dort nicht mehr als Kandidat für das Autowiring in Betracht.
Für den Service child ließe sich autowired: ChildClass auch als autowired: self
schreiben, denn self ist ein Platzhalter für die Klasse des aktuellen Services.
Im Schlüssel autowired lassen sich auch mehrere Klassen oder Interfaces als Array angeben:
autowired: [ParentClass, FooInterface]
Versuchen wir, dem Beispiel Interfaces hinzuzufügen:
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)
{}
}
Schränken wir den Service child in keiner Weise ein, passt er in die Konstruktoren aller Klassen
FooDependent, BarDependent, ParentDependent und ChildDependent, und das
Autowiring übergibt ihn dorthin.
Schränken wir sein Autowiring jedoch mit autowired: ChildClass (oder self) auf
ChildClass ein, übergibt es ihn nur dem Konstruktor von ChildDependent, weil dieser ein Argument vom
Typ ChildClass verlangt und gilt, dass ChildClass vom Typ ChildClass ist. Bei
keinem der anderen Parameter ist der verlangte Typ ChildClass oder ein Untertyp davon, der Service wird ihnen also
nicht übergeben.
Schränken wir ihn mit autowired: ParentClass auf ParentClass ein, übergibt ihn das Autowiring
wieder dem Konstruktor von ChildDependent (weil die verlangte ChildClass ein Untertyp von
ParentClass ist) und jetzt auch dem Konstruktor von ParentDependent, denn der verlangte Typ
ParentClass passt ebenfalls.
Schränken wir ihn auf FooInterface ein, wird er weiterhin in ParentDependent (die verlangte
ParentClass ist ein Untertyp von FooInterface) und in ChildDependent autowiret, zusätzlich
auch in den Konstruktor von FooDependent, nicht aber in BarDependent, weil BarInterface
kein Untertyp von FooInterface ist.
services:
child:
create: ChildClass
autowired: FooInterface
fooDep: FooDependent # das Autowiring übergibt dem Konstruktor den Service child
barDep: BarDependent # WIRFT EINE EXCEPTION, kein Service passt
parentDep: ParentDependent # das Autowiring übergibt dem Konstruktor den Service child
childDep: ChildDependent # das Autowiring übergibt dem Konstruktor den Service child