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