Übergabe von Abhängigkeiten
Argumente, oder “Abhängigkeiten” in der DI-Terminologie, können auf die folgenden Hauptwege an Klassen übergeben werden:
- Übergabe per Konstruktor
- Übergabe durch eine Methode (Setter genannt)
- durch das Setzen einer Eigenschaft
- durch Methode, Annotation oder Attribut inject
Wir werden nun die verschiedenen Varianten mit konkreten Beispielen illustrieren.
Konstruktor-Injektion
Abhängigkeiten werden als Argumente an den Konstruktor übergeben, wenn das Objekt erstellt wird:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Diese Form ist nützlich für obligatorische Abhängigkeiten, die die Klasse unbedingt benötigt, um zu funktionieren, da ohne sie die Instanz nicht erstellt werden kann.
Seit PHP 8.0 können wir eine kürzere Form der Notation verwenden (constructor property promotion), die funktional gleichwertig ist:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Seit PHP 8.1 kann eine Eigenschaft mit einem Flag readonly
markiert werden, das besagt, dass sich der Inhalt der
Eigenschaft nicht ändern wird:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
Der DI-Container übergibt Abhängigkeiten automatisch an den Konstruktor mittels Autowiring. Argumente, die nicht auf diese Weise übergeben werden können (z.B. Strings, Zahlen, Booleans), werden in die Konfiguration geschrieben.
Konstrukteur-Hölle
Der Begriff Konstruktorhölle bezieht sich auf eine Situation, in der ein Kind von einer Elternklasse erbt, deren Konstruktor Abhängigkeiten benötigt, und das Kind benötigt ebenfalls Abhängigkeiten. Es muss auch die Abhängigkeiten der Elternklasse übernehmen und weitergeben:
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 tritt auf, wenn wir den Konstruktor der Klasse BaseClass
ändern wollen, zum Beispiel wenn eine neue
Abhängigkeit hinzugefügt wird. Dann müssen wir auch alle Konstruktoren der Kinder ändern. Das macht eine solche Änderung
zur Hölle.
Wie kann man dies verhindern? Die Lösung besteht darin, [Zusammensetzung gegenüber Vererbung |faq#Why composition is preferred over inheritance] zu bevorzugen.
Lassen Sie uns also den Code anders gestalten. Wir werden abstrakte Base*
Klassen vermeiden. Anstatt dass MyClass
einige Funktionen durch Vererbung von BaseClass
erhält, wird
diese Funktionalität 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-Injektion
Abhängigkeiten werden durch den Aufruf einer Methode übergeben, die sie in einer privaten Eigenschaft speichert. Die übliche
Namenskonvention für diese Methoden ist die Form set*()
, weshalb sie auch Setter genannt werden, aber natürlich
können sie auch anders heißen.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Diese Methode ist nützlich für optionale Abhängigkeiten, die für die Funktion der Klasse nicht notwendig sind, da nicht garantiert ist, dass das Objekt sie tatsächlich erhält (d. h. dass der Benutzer die Methode aufruft).
Gleichzeitig ermöglicht diese Methode, dass der Setter wiederholt aufgerufen werden kann, um die Abhängigkeit zu ändern.
Wenn dies nicht erwünscht ist, fügen Sie der Methode ein Häkchen hinzu, oder markieren Sie ab PHP 8.1 die Eigenschaft
$cache
mit dem Flag readonly
.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if ($this->cache) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
Der Setter-Aufruf wird in der DI-Container-Konfiguration im Abschnitt setup definiert. Auch hier wird die automatische Übergabe von Abhängigkeiten durch Autowiring genutzt:
services:
- create: MyClass
setup:
- setCache
Property Injection
Abhängigkeiten werden direkt an die Eigenschaft übergeben:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Diese Methode wird als ungeeignet angesehen, da die Eigenschaft als public
deklariert werden muss. Daher haben wir
keine Kontrolle darüber, ob die übergebene Abhängigkeit tatsächlich vom angegebenen Typ ist (dies war vor PHP 7.4 der Fall),
und wir verlieren die Möglichkeit, auf die neu zugewiesene Abhängigkeit mit unserem eigenen Code zu reagieren, um zum Beispiel
nachträgliche Änderungen zu verhindern. Gleichzeitig wird die Eigenschaft Teil der öffentlichen Schnittstelle der Klasse, was
möglicherweise nicht wünschenswert ist.
Die Einstellung der Variablen wird in der Konfiguration des DI-Containers im Abschnitt setup festgelegt:
services:
- create: MyClass
setup:
- $cache = @\Cache
Einspritzen
Während die drei vorangegangenen Methoden allgemein in allen objektorientierten Sprachen gültig sind, ist das Injizieren per Methode, Annotation oder inject-Attribut spezifisch für Nette-Präsentatoren. Sie werden in einem separaten Kapitel behandelt.
Welcher Weg soll gewählt werden?
- Der Konstruktor eignet sich für obligatorische Abhängigkeiten, die die Klasse zum Funktionieren benötigt.
- der Setter hingegen eignet sich für optionale oder veränderbare Abhängigkeiten
- öffentliche Variablen werden nicht empfohlen