Abhängigkeiten übergeben
Argumente, in der Terminologie der DI “Abhängigkeiten”, lassen sich Klassen auf diese Hauptarten übergeben:
- Übergabe im Konstruktor (Constructor Injection)
- Übergabe über eine Methode (sogenannte Setter Injection)
- Setzen einer Property (Property Injection)
- Über die Methode
inject*()oder das Attribut#[Inject]
Zeigen wir jede Variante an konkreten Beispielen.
Übergabe im Konstruktor
Die Abhängigkeiten werden beim Erzeugen des Objekts als Argumente des Konstruktors übergeben:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Dieser Weg eignet sich für zwingende Abhängigkeiten, die die Klasse für ihren Betrieb unbedingt braucht, denn ohne sie lässt sich die Instanz nicht erzeugen.
Seit PHP 8.0 können wir eine kürzere Schreibweise verwenden (Constructor Property Promotion), die funktional gleichwertig ist:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Seit PHP 8.1 lässt sich eine Property mit dem Flag readonly kennzeichnen, das erklärt, dass sich der Wert der
Property nach der Initialisierung nicht mehr ändert:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
Der DI-Container übergibt dem Konstruktor die Abhängigkeiten automatisch über Autowiring. Argumente, die sich so nicht übergeben lassen (etwa Strings, Zahlen, boolesche Werte), gibt man in der Konfiguration an.
Constructor Hell
Der Begriff Constructor Hell beschreibt die Situation, dass eine Kindklasse von einer Elternklasse erbt, deren Konstruktor Abhängigkeiten verlangt, und die Kindklasse ebenfalls Abhängigkeiten verlangt. Sie muss dann auch die Abhängigkeiten des Elternteils entgegennehmen und weiterreichen:
abstract class BaseClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass extends BaseClass
{
private Database $db;
// ⛔ CONSTRUCTOR HELL
public function __construct(Cache $cache, Database $db)
{
parent::__construct($cache);
$this->db = $db;
}
}
Das Problem zeigt sich, wenn wir den Konstruktor von BaseClass ändern wollen, etwa weil eine neue Abhängigkeit
hinzukommt. Dann müssen auch alle Konstruktoren der Kindklassen angepasst werden. Was eine solche Änderung zur
Hölle macht.
Wie lässt sich dem vorbeugen? Die Lösung ist, Komposition der Vererbung vorzuziehen.
Wir entwerfen den Code also anders. Wir verzichten auf abstrakte
Base*-Klassen. Statt dass MyClass bestimmte Fähigkeiten erbt, indem sie von BaseClass
abgeleitet wird, bekommt sie diese Fähigkeiten als Abhängigkeit übergeben:
final class SomeFunctionality
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass
{
private SomeFunctionality $sf;
private Database $db;
public function __construct(SomeFunctionality $sf, Database $db) // ✅
{
$this->sf = $sf;
$this->db = $db;
}
}
Setter Injection
Die Abhängigkeiten werden übergeben, indem eine Methode aufgerufen wird, die sie in einer privaten Property ablegt. Diese
Methoden werden üblicherweise nach dem Muster set*() benannt, weshalb man sie Setter nennt, sie können aber
natürlich auch anders heißen.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Dieser Weg eignet sich für optionale Abhängigkeiten, die für den Betrieb der Klasse nicht unerlässlich sind, denn es ist nicht garantiert, dass das Objekt die Abhängigkeit tatsächlich bekommt (also dass der Aufrufer die Methode aufruft).
Zugleich erlaubt dieser Weg, den Setter wiederholt aufzurufen und die Abhängigkeit zu ändern. Ist das unerwünscht, ergänzen
Sie in der Methode eine Prüfung oder kennzeichnen Sie die Property $cache seit PHP 8.1 mit dem Flag
readonly.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if (isset($this->cache)) {
throw new RuntimeException('Die Abhängigkeit wurde bereits gesetzt');
}
$this->cache = $cache;
}
}
Der Aufruf des Setters wird in der Konfiguration des DI-Containers im Schlüssel setup festgelegt. Auch hier werden die Abhängigkeiten automatisch über Autowiring übergeben:
services:
- create: MyClass
setup:
- setCache
Property Injection
Die Abhängigkeiten werden übergeben, indem direkt in eine Property geschrieben wird:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Dieser Weg gilt als ungeeignet, weil die Property als public deklariert werden muss. Dadurch verlieren wir die
Kontrolle darüber, dass die übergebene Abhängigkeit tatsächlich den verlangten Typ hat (das galt besonders vor den
Typdeklarationen für Properties in PHP 7.4), und wir verlieren die Möglichkeit, auf eine neu zugewiesene Abhängigkeit mit
eigener Logik zu reagieren, etwa um eine spätere Änderung zu verhindern. Zugleich wird die Property Teil der öffentlichen API
der Klasse, was womöglich nicht beabsichtigt ist.
Die Zuweisung an die Property wird in der Konfiguration des DI-Containers im Abschnitt setup festgelegt:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
Während die vorigen drei Wege allgemein in allen objektorientierten Sprachen gelten, wird die Übergabe über die Methoden
inject*() oder das Attribut #[Inject] üblicherweise bei Nette-Presentern verwendet, wo sie
standardmäßig eingeschaltet ist; jeder andere Service kann sie über inject: true einschalten.
Behandelt werden sie in einem eigenen Kapitel.
Welchen Weg wählen?
- Der Konstruktor eignet sich für zwingende Abhängigkeiten, die die Klasse für ihren Betrieb unbedingt braucht.
- Der Setter eignet sich umgekehrt für optionale Abhängigkeiten oder für solche, die sich später ändern lassen sollen.
- Von öffentlichen Properties ist allgemein abzuraten.