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)