Generierte Factories

Nette DI kann den Code von Factories automatisch anhand von Interfaces erzeugen und erspart Ihnen damit das Schreiben von Code.

Eine Factory ist eine Klasse, die dafür zuständig ist, Objekte zu erzeugen und ihnen ihre Abhängigkeiten zu übergeben. Bitte verwechseln Sie das nicht mit dem Entwurfsmuster Factory Method, das eine bestimmte Art beschreibt, Factories zu nutzen, und mit diesem Thema nichts zu tun hat.

Wie eine solche Factory aussieht, haben wir im einführenden Kapitel 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 einer Factory automatisch erzeugen. Sie müssen nur ein Interface anlegen, und Nette DI erzeugt die Implementierung. Das Interface muss genau eine Methode namens create haben und einen Rückgabetyp deklarieren:

interface ArticleFactory
{
	function create(): Article;
}

Die Factory ArticleFactory hat also eine Methode create, die Article-Objekte erzeugt. Die Klasse Article könnte zum Beispiel so aussehen:

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

Fügen Sie die Factory der Konfigurationsdatei hinzu:

services:
	- ArticleFactory

Nette DI erzeugt die passende Implementierung der Factory.

In dem Code, der die Factory verwendet, fordern Sie das Objekt über sein Interface an, und Nette DI liefert die erzeugte Implementierung:

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

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

Factory mit Parametern

Die Methode create der Factory kann Parameter entgegennehmen, die sie dann an den Konstruktor weiterreicht. Ergänzen wir die Klasse Article zum Beispiel um die ID des Autors des Artikels:

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

Den Parameter ergänzen wir auch in der Factory:

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

Weil der Name des Parameters im Konstruktor ($authorId) mit dem Namen des Parameters in der Methode der Factory übereinstimmt, übergibt Nette DI ihn automatisch.

Erweiterte Definition

Die Definition lässt sich über den Schlüssel implement auch mehrzeilig schreiben:

services:
	articleFactory:
		implement: ArticleFactory

Diese längere Schreibweise erlaubt es, über den Schlüssel arguments weitere Argumente für den Konstruktor anzugeben und über setup weiter zu konfigurieren, ganz wie bei gewöhnlichen Service-Definitionen.

Ein Beispiel: Nähme die Methode create() den Parameter $authorId nicht entgegen, könnten wir in der Konfiguration einen festen Wert angeben, der dem Konstruktor von Article übergeben wird:

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

Nähme create() umgekehrt $authorId entgegen, wäre der Wert aber nicht Teil des Konstruktors, sondern würde über eine Methode wie Article::setAuthorId() übergeben, verweisen wir im Abschnitt setup auf den Parameter:

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

Accessor

Neben Factories kann Nette auch sogenannte Accessors erzeugen. Das sind Objekte mit einer Methode get(), die einen bestimmten Service aus dem DI-Container zurückgibt. Wiederholte Aufrufe von get() liefern immer dieselbe Instanz.

Accessors bieten Lazy Loading für Abhängigkeiten. Stellen Sie sich eine Klasse vor, die Fehler in eine eigene Datenbank protokolliert. Bekäme diese Klasse die Datenbankverbindung über Dependency Injection im Konstruktor, würde die Verbindung immer aufgebaut, auch wenn Fehler selten auftreten und die Verbindung die meiste Zeit ungenutzt bleibt. Stattdessen kann die Klasse einen Accessor bekommen. Das Objekt der Datenbank (die Verbindung) entsteht erst dann, wenn die Methode get() des Accessors zum ersten Mal aufgerufen wird.

Wie erzeugt man einen Accessor? Schreiben Sie einfach ein Interface, und Nette DI erzeugt die Implementierung. Das Interface muss genau eine Methode namens get haben, die keine Parameter entgegennimmt und den Rückgabetyp deklariert:

interface PDOAccessor
{
	function get(): PDO;
}

Fügen Sie den Accessor der Konfigurationsdatei hinzu, zusammen mit der Definition des Services, den er zurückgeben soll:

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

Weil der Accessor einen PDO-Service zurückgibt und in der Konfiguration nur ein einziger solcher Service definiert ist, gibt der Accessor genau diesen Service zurück. Gibt es mehrere Services dieses Typs, geben Sie über den Namen an, welchen der Accessor zurückgeben soll, etwa - PDOAccessor(@db1).

Multifactory/Accessor

Bisher konnten unsere Factories und Accessors nur einen einzigen Typ von Objekt erzeugen bzw. zurückgeben. Sie können aber leicht Multifactories bauen, die die Fähigkeiten von Factories und Accessors vereinen. Das Interface einer solchen Komponente kann mehrere Methoden namens create<Name>() und get<Name>() enthalten, zum Beispiel:

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

Statt mehrere einzelne Factories und Accessors zu injizieren, können Sie also eine einzige, umfassendere Komponente injizieren.

Alternativ lässt sich statt mehrerer Methoden ein get() mit einem Parameter verwenden:

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

Dann tut MultiFactory::getDb() dasselbe wie MultiFactoryAlt::get('db'). Diese alternative Schreibweise hat allerdings den Nachteil, dass aus der Signatur des Interfaces nicht ausdrücklich hervorgeht, welche Werte für $name unterstützt werden. Außerdem lassen sich im Interface für verschiedene Werte von $name keine unterschiedlichen Rückgabetypen festlegen.

Statt get($name) kann das Interface create($name) deklarieren, das bei jedem Aufruf eine neue Instanz zurückgibt (während get() eine gemeinsame liefert). Das Interface darf nur eine einzige solche Methode mit Parameter enthalten. Ist der Rückgabetyp der Methode nullable (etwa ?PDO), gibt sie bei einem unbekannten $name statt einer Exception den Wert null zurück.

Definition über eine Liste

Eine Multifactory lässt sich in der Konfiguration über eine Liste definieren, wobei die Services inline geschrieben werden:

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

Alternativ können Sie in der Definition der Multifactory über Referenzen auf bestehende Services verweisen:

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

Definition über Tags

Eine weitere Möglichkeit, eine Multifactory zu definieren, sind Tags. Der Wert des Tags bestimmt den Namen der zugehörigen Methode:

services:
	article:
		create: Article
		tags: {multi: article}     # definiert createArticle()
	db:
		create: PDO(%dsn%, %user%, %password%)
		tags: {multi: db}          # definiert getDb()

	- MultiFactory(tagged: multi)
Version: 3.x