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
Version: 3.x