Was ist ein DI-Container?

Ein Dependency-Injection-Container (DIC oder DI-Container) ist ein Objekt, das dafür zuständig ist, andere Objekte (Services genannt) zu erzeugen und zu konfigurieren.

Es mag Sie überraschen, aber in vielen Fällen brauchen Sie keinen Dependency-Injection-Container, um die Vorteile der Dependency Injection (kurz DI) zu nutzen. Schließlich haben wir schon im einführenden Kapitel konkrete Beispiele für DI gezeigt, und kein Container war nötig.

Sobald Sie aber eine große Zahl von Objekten mit komplexen Abhängigkeiten verwalten, wird ein DI-Container sehr nützlich. Das ist bei Webanwendungen auf Basis eines Frameworks oft der Fall.

Im vorigen Kapitel haben wir die Klassen Article und EditController vorgestellt. Beide haben Abhängigkeiten, nämlich die Datenbank und die Factory ArticleFactory. Und für diese Klassen bauen wir jetzt einen Container. Für ein so einfaches Beispiel ist ein Container natürlich mit Kanonen auf Spatzen geschossen. Wir bauen ihn aber, um zu zeigen, wie er aussieht und funktioniert.

Hier ist ein einfacher, fest verdrahteter Container für das obige Beispiel:

class Container
{
	public function createDatabase(): Nette\Database\Connection
	{
		return new Nette\Database\Connection('mysql:', 'root', '***');
	}

	public function createArticleFactory(): ArticleFactory
	{
		return new ArticleFactory($this->createDatabase());
	}

	public function createEditController(): EditController
	{
		return new EditController($this->createArticleFactory());
	}
}

Die Verwendung sähe so aus:

$container = new Container;
$controller = $container->createEditController();

Wir fordern das Objekt einfach beim Container an, ohne wissen zu müssen, wie es zu erzeugen ist oder welche Abhängigkeiten es hat; das alles erledigt der Container. Die Abhängigkeiten übergibt er automatisch. Das ist seine Stärke.

Derzeit hat der Container alle Informationen fest verdrahtet. Gehen wir also einen Schritt weiter und ergänzen Parameter, damit der Container wirklich nützlich wird:

class Container
{
	public function __construct(
		private array $parameters,
	) {
	}

	public function createDatabase(): Nette\Database\Connection
	{
		return new Nette\Database\Connection(
			$this->parameters['db.dsn'],
			$this->parameters['db.user'],
			$this->parameters['db.password'],
		);
	}

	// ...
}

$container = new Container([
	'db.dsn' => 'mysql:',
	'db.user' => 'root',
	'db.password' => '***',
]);

Aufmerksamen Lesern fällt vielleicht ein Problem auf. Jedes Mal, wenn wir ein EditController-Objekt holen, entstehen auch neue Instanzen von ArticleFactory und der Datenbankverbindung. Das wollen wir ganz sicher nicht.

Wir ergänzen deshalb eine Methode getService(), die immer dieselben Instanzen zurückgibt:

class Container
{
	private array $services = [];

	public function __construct(
		private array $parameters,
	) {
	}

	public function getService(string $name): object
	{
		if (!isset($this->services[$name])) {
			// getService('Database') ruft createDatabase() auf
			$method = 'create' . $name;
			$this->services[$name] = $this->$method();
		}
		return $this->services[$name];
	}

	// ...
}

Beim ersten Aufruf, etwa $container->getService('Database'), ruft sie createDatabase() auf, um das Objekt der Datenbank zu erzeugen, legt es im Array $services ab und gibt es zurück. Bei weiteren Aufrufen gibt sie die bereits abgelegte Instanz direkt zurück.

Auch den Rest des Containers passen wir so an, dass er getService() verwendet:

class Container
{
	// ...

	public function createArticleFactory(): ArticleFactory
	{
		return new ArticleFactory($this->getService('Database'));
	}

	public function createEditController(): EditController
	{
		return new EditController($this->getService('ArticleFactory'));
	}
}

Übrigens bezeichnet der Begriff Service jedes Objekt, das der Container verwaltet. Daher der Name der Methode getService().

Fertig. Wir haben einen voll funktionsfähigen DI-Container! Und wir können ihn verwenden:

$container = new Container([
	'db.dsn' => 'mysql:',
	'db.user' => 'root',
	'db.password' => '***',
]);

$controller = $container->getService('EditController');
$database = $container->getService('Database');

Wie Sie sehen, ist es nicht schwer, einen DIC zu schreiben. Bemerkenswert ist, dass die Objekte selbst nichts davon wissen, dass ein Container sie erzeugt. Folglich lässt sich auf diese Weise jedes PHP-Objekt erzeugen, ohne seinen Quellcode zu ändern.

Die Klasse des Containers von Hand zu erzeugen und zu pflegen kann schnell zum Albtraum werden. Deshalb sprechen wir im nächsten Kapitel über den Nette DI Container, der sich fast vollständig selbst erzeugen und aktualisieren kann.

Version: 3.x