Generierte Fabriken

Nette DI kann automatisch Code für Fabriken basierend auf Schnittstellen generieren, was Ihnen das Schreiben von Code erspart.

Eine Fabrik ist eine Klasse, die Objekte herstellt und konfiguriert. Sie übergibt ihnen also auch ihre Abhängigkeiten. Bitte verwechseln Sie dies nicht mit dem Entwurfsmuster Factory Method, das eine spezifische Art der Verwendung von Fabriken beschreibt und mit diesem Thema nichts zu tun hat.

Wie eine solche Fabrik aussieht, haben wir im Einführungskapitel gezeigt:

class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}

Nette DI kann den Code von Fabriken automatisch generieren. Alles, was Sie tun müssen, ist, eine Schnittstelle zu erstellen, und Nette DI generiert die Implementierung. Die Schnittstelle muss genau eine Methode namens create haben und einen Rückgabetyp deklarieren:

interface ArticleFactory
{
	function create(): Article;
}

Die Fabrik ArticleFactory hat also eine Methode create, die Article-Objekte erstellt. Die Klasse Article könnte beispielsweise so aussehen:

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}
}

Wir fügen die Fabrik zur Konfigurationsdatei hinzu:

services:
	- ArticleFactory

Nette DI generiert die entsprechende Implementierung der Fabrik.

Im Code, der die Fabrik verwendet, fordern wir das Objekt über die Schnittstelle an, und Nette DI verwendet die generierte Implementierung:

class UserController
{
	public function __construct(
		private ArticleFactory $articleFactory,
	) {
	}

	public function foo()
	{
		// lassen wir die Fabrik das Objekt erstellen
		$article = $this->articleFactory->create();
	}
}

Parametrisierte Fabrik

Die Fabrikmethode create kann Parameter annehmen, die sie dann an den Konstruktor weitergibt. Ergänzen wir beispielsweise die Klasse Article um die ID des Artikelautors:

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
		private int $authorId,
	) {
	}
}

Wir fügen den Parameter auch zur Fabrik hinzu:

interface ArticleFactory
{
	function create(int $authorId): Article;
}

Da der Parameter im Konstruktor und der Parameter in der Fabrik denselben Namen haben, übergibt Nette DI sie vollautomatisch.

Erweiterte Definition

Die Definition kann auch in mehrzeiliger Form unter Verwendung des Schlüssels implement geschrieben werden:

services:
	articleFactory:
		implement: ArticleFactory

Bei dieser längeren Schreibweise können zusätzliche Argumente für den Konstruktor im Schlüssel arguments und zusätzliche Konfigurationen mittels setup angegeben werden, genau wie bei regulären Diensten.

Beispiel: Wenn die Methode create() den Parameter $authorId nicht akzeptieren würde, könnten wir einen festen Wert in der Konfiguration angeben, der an den Konstruktor von Article übergeben würde:

services:
	articleFactory:
		implement: ArticleFactory
		arguments:
			authorId: 123

Oder umgekehrt, wenn create() den Parameter $authorId akzeptieren würde, aber er nicht Teil des Konstruktors wäre und über die Methode Article::setAuthorId() übergeben würde, würden wir im Abschnitt setup darauf verweisen:

services:
	articleFactory:
		implement: ArticleFactory
		setup:
			- setAuthorId($authorId)

Accessor

Nette kann neben Fabriken auch sogenannte Accessoren generieren. Dies sind Objekte mit einer get()-Methode, die einen bestimmten Dienst aus dem DI-Container zurückgibt. Wiederholte Aufrufe von get() geben immer dieselbe Instanz zurück.

Accessoren ermöglichen Lazy-Loading für Abhängigkeiten. Nehmen wir an, wir haben eine Klasse, die Fehler in eine spezielle Datenbank schreibt. Wenn diese Klasse die Datenbankverbindung als Abhängigkeit im Konstruktor übergeben bekäme, müsste die Verbindung immer erstellt werden, obwohl in der Praxis ein Fehler nur selten auftritt und die Verbindung daher meist ungenutzt bliebe. Stattdessen übergibt sich die Klasse einen Accessor, und erst wenn dessen get() aufgerufen wird, wird das Datenbankobjekt erstellt:

Wie erstellt man einen Accessor? Schreiben Sie einfach eine Schnittstelle, und Nette DI generiert die Implementierung. Die Schnittstelle muss genau eine Methode namens get haben und einen Rückgabetyp deklarieren:

interface PDOAccessor
{
	function get(): PDO;
}

Wir fügen den Accessor zur Konfigurationsdatei hinzu, wo auch die Definition des Dienstes steht, den er zurückgeben wird:

services:
	- PDOAccessor
	- PDO(%dsn%, %user%, %password%)

Da der Accessor einen Dienst vom Typ PDO zurückgibt und in der Konfiguration nur ein solcher Dienst vorhanden ist, wird er genau diesen zurückgeben. Wenn es mehrere Dienste dieses Typs gäbe, würden wir den zurückgegebenen Dienst anhand seines Namens bestimmen, z. B. - PDOAccessor(@db1).

Mehrfachfabrik/-accessor

Unsere Fabriken und Accessoren konnten bisher immer nur ein Objekt herstellen oder zurückgeben. Es ist jedoch sehr einfach, auch Mehrfachfabriken in Kombination mit Accessoren zu erstellen. Die Schnittstelle einer solchen Klasse enthält eine beliebige Anzahl von Methoden mit den Namen create<name>() und get<name>(), z. B.:

interface MultiFactory
{
	function createArticle(): Article;
	function getDb(): PDO;
}

Anstatt also mehrere generierte Fabriken und Accessoren zu übergeben, übergeben wir eine komplexere Fabrik, die mehr kann.

Alternativ kann anstelle mehrerer Methoden get() mit einem Parameter verwendet werden:

interface MultiFactoryAlt
{
	function get($name): PDO;
}

Dann gilt, dass MultiFactory::getArticle() dasselbe tut wie MultiFactoryAlt::get('article'). Die alternative Schreibweise hat jedoch den Nachteil, dass nicht ersichtlich ist, welche Werte für $name unterstützt werden, und logischerweise können in der Schnittstelle auch keine unterschiedlichen Rückgabewerte für verschiedene $name unterschieden werden.

Definition durch Liste

Auf diese Weise kann eine Mehrfachfabrik in der Konfiguration definiert werden:

services:
	- MultiFactory(
		article: Article                      # definiert createArticle()
		db: PDO(%dsn%, %user%, %password%)    # definiert getDb()
	)

Oder wir können uns in der Fabrikdefinition mittels Referenz auf bestehende Dienste beziehen:

services:
	article: Article
	- PDO(%dsn%, %user%, %password%)
	- MultiFactory(
		article: @article    # definiert createArticle()
		db: @\PDO            # definiert getDb()
	)

Definition mittels Tags

Die zweite Möglichkeit ist, zur Definition Tags zu verwenden:

services:
	- App\Core\RouterFactory::createRouter
	- App\Model\DatabaseAccessor(
		db1: @database.db1.explorer
	)
Version: 3.x