Was ist ein DI Container?

Ein Dependency Injection Container (DIC) ist eine Klasse, die Objekte instanziieren und konfigurieren kann.

Es mag Sie überraschen, aber in vielen Fällen benötigen Sie keinen Dependency Injection Container, um die Vorteile der Dependency Injection (kurz DI) zu nutzen. Schon im Einführungskapitel haben wir DI anhand konkreter Beispiele gezeigt, und es wurde kein Container benötigt.

Wenn Sie jedoch eine große Anzahl verschiedener Objekte mit vielen Abhängigkeiten verwalten müssen, wird ein Dependency Injection Container wirklich nützlich sein. Dies ist beispielsweise bei Webanwendungen der Fall, die auf einem Framework basieren.

Im vorherigen Kapitel haben wir die Klassen Article und UserController vorgestellt. Beide haben einige Abhängigkeiten, nämlich die Datenbank und die Fabrik ArticleFactory. Und für diese Klassen werden wir nun einen Container erstellen. Natürlich ist es für ein so einfaches Beispiel nicht sinnvoll, einen Container zu haben. Aber wir werden ihn erstellen, um zu zeigen, wie er aussieht und funktioniert.

Hier ist ein einfacher hardcodierter Container für das gegebene 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 createUserController(): UserController
	{
		return new UserController($this->createArticleFactory());
	}
}

Die Verwendung würde folgendermaßen aussehen:

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

Wir fragen den Container nur nach dem Objekt und müssen nichts mehr darüber wissen, wie es erstellt wird oder welche Abhängigkeiten es hat; das alles weiß der Container. Die Abhängigkeiten werden vom Container automatisch injiziert. Darin liegt seine Stärke.

Bisher hat der Container alle Daten fest codiert. Wir werden also den nächsten Schritt machen und Parameter hinzufügen, 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' => '***',
]);

Aufmerksame Leser haben vielleicht ein Problem bemerkt. Jedes Mal, wenn ich ein UserController-Objekt erhalte, werden auch eine neue Instanz von ArticleFactory und der Datenbank erstellt. Das wollen wir definitiv nicht.

Wir fügen daher eine Methode getService() hinzu, 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 von z. B. $container->getService('Database') lässt sie von createDatabase() das Datenbankobjekt erstellen, speichert es im Array $services und gibt es beim nächsten Aufruf direkt zurück.

Wir passen auch den Rest des Containers an, um getService() zu verwenden:

class Container
{
	// ...

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

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

Übrigens wird der Begriff Dienst (Service) für jedes Objekt verwendet, das vom Container verwaltet wird. Daher auch 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('UserController');
$database = $container->getService('Database');

Wie Sie sehen, ist es nicht kompliziert, einen DIC zu schreiben. Es sei daran erinnert, dass die Objekte selbst nicht wissen, dass sie von einem Container erstellt werden. Daher ist es möglich, jedes Objekt in PHP auf diese Weise zu erstellen, ohne seinen Quellcode zu ändern.

Das manuelle Erstellen und Warten einer Containerklasse kann schnell zu einem Albtraum werden. Im nächsten Kapitel werden wir daher über den Nette DI Container sprechen, der sich fast von selbst generieren und aktualisieren kann.

Version: 3.x