Übergeben von Abhängigkeiten
Argumente, oder in der DI-Terminologie „Abhängigkeiten“, können auf folgende Hauptarten an Klassen übergeben werden:
- Übergabe per Konstruktor
- Übergabe per Methode (sog. Setter)
- Zuweisung zu einer Variablen
- Methode, Annotation oder Attribut inject
Nun werden wir die einzelnen Varianten anhand konkreter Beispiele erläutern.
Übergabe per Konstruktor
Abhängigkeiten werden im Moment der Objekterstellung als Argumente des Konstruktors übergeben:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Diese Form eignet sich für obligatorische Abhängigkeiten, die die Klasse unbedingt für ihre Funktion benötigt, da ohne sie keine Instanz erstellt werden kann.
Seit PHP 8.0 können wir eine kürzere Schreibweise verwenden (constructor property promotion), die funktional äquivalent ist:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Seit PHP 8.1 kann die Variable mit dem Flag readonly
markiert werden, was deklariert, dass sich der Inhalt der
Variablen nicht mehr ä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 auf diese Weise nicht übergeben werden können (z. B. Zeichenketten, Zahlen, Booleans), schreiben wir in die Konfiguration.
Constructor hell
Der Begriff constructor hell bezeichnet eine Situation, in der ein Kind von einer Elternklasse erbt, deren Konstruktor Abhängigkeiten erfordert, und gleichzeitig das Kind Abhängigkeiten erfordert. Dabei muss es auch die elterlichen übernehmen und übergeben:
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 hinzukommt. Dann müssen nämlich auch alle Konstruktoren der Kinder angepasst werden. Was eine solche Anpassung zur
Hölle macht.
Wie kann man dem vorbeugen? Die Lösung ist, Komposition der Vererbung vorzuziehen.
Also entwerfen wir den Code anders. Wir werden abstrakte Base*
Klassen vermeiden. Anstatt dass MyClass
bestimmte Funktionalität durch Erben von BaseClass
erhält,
lässt sie sich 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;
}
}
Übergabe per Setter
Abhängigkeiten werden durch Aufruf einer Methode übergeben, die sie in einer privaten Variablen speichert. Die übliche
Namenskonvention für diese Methoden ist die Form set*()
, daher werden sie Setter genannt, aber sie können
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);
Diese Methode eignet sich für optionale Abhängigkeiten, die für die Funktion der Klasse nicht notwendig sind, da nicht garantiert ist, dass das Objekt die Abhängigkeit tatsächlich erhält (d. h. dass der Benutzer die Methode aufruft).
Gleichzeitig erlaubt diese Methode, den Setter wiederholt aufzurufen und die Abhängigkeit so zu ändern. Wenn dies nicht
erwünscht ist, fügen wir der Methode eine Prüfung hinzu oder markieren ab PHP 8.1 die Property $cache
mit dem
Flag readonly
.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if (isset($this->cache)) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
Der Aufruf des Setters wird in der Konfiguration des DI-Containers im Schlüssel setup definiert. Auch hier wird die automatische Übergabe von Abhängigkeiten mittels Autowiring genutzt:
services:
- create: MyClass
setup:
- setCache
Zuweisung zu einer Variablen
Abhängigkeiten werden durch Schreiben direkt in eine Mitgliedsvariable übergeben:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Diese Methode wird als ungeeignet angesehen, da die Mitgliedsvariable als public
deklariert werden muss. Dadurch
haben wir keine Kontrolle darüber, dass die übergebene Abhängigkeit tatsächlich vom angegebenen Typ ist (galt vor PHP 7.4),
und wir verlieren die Möglichkeit, auf die neu zugewiesene Abhängigkeit mit eigenem Code zu reagieren, beispielsweise um eine
nachfolgende Änderung zu verhindern. Gleichzeitig wird die Variable Teil der öffentlichen Schnittstelle der Klasse, was
möglicherweise nicht erwünscht ist.
Die Zuweisung der Variablen wird in der Konfiguration des DI-Containers im Abschnitt setup definiert:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
Während die vorherigen drei Methoden allgemein in allen objektorientierten Sprachen gelten, ist das Injizieren per Methode, Annotation oder Attribut inject spezifisch für Presenter in Nette. Ein separates Kapitel behandelt sie.
Welche Methode wählen?
- Der Konstruktor eignet sich für obligatorische Abhängigkeiten, die die Klasse unbedingt für ihre Funktion benötigt.
- Der Setter eignet sich hingegen für optionale Abhängigkeiten oder Abhängigkeiten, die man weiter ändern können möchte.
- Öffentliche Variablen sind nicht geeignet.