Factory generate

Nette DI sa generare automaticamente il codice delle factory a partire dalle interfacce, risparmiandovi di scrivere codice.

Una factory è una classe che si occupa di creare oggetti e di passarne le dipendenze. Non confondetela con il design pattern factory method, che descrive un modo specifico di usare le factory e non ha nulla a che vedere con questo argomento.

Abbiamo mostrato che aspetto ha una factory del genere nel capitolo introduttivo:

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

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

Nette DI sa generare automaticamente il codice della factory. Vi basta creare un'interfaccia e Nette DI ne genererà l'implementazione. L'interfaccia deve avere esattamente un metodo chiamato create e dichiarare un tipo di ritorno:

interface ArticleFactory
{
	function create(): Article;
}

La factory ArticleFactory ha quindi un metodo create che crea oggetti Article. La classe Article potrebbe avere per esempio questo aspetto:

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

Aggiungete la factory al file di configurazione:

services:
	- ArticleFactory

Nette DI genererà la corrispondente implementazione della factory.

Nel codice che usa la factory, chiedete l'oggetto tramite la sua interfaccia e Nette DI vi fornirà l'implementazione generata:

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

	public function foo()
	{
		// lascia che sia la factory a creare l'oggetto
		$article = $this->articleFactory->create();
	}
}

Factory con parametri

Il metodo create della factory può accettare parametri, che passa poi al costruttore. Aggiungiamo per esempio alla classe Article l'ID dell'autore dell'articolo:

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

Aggiungiamo il parametro anche alla factory:

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

Poiché il nome del parametro nel costruttore ($authorId) coincide con il nome del parametro nel metodo della factory, Nette DI lo passa automaticamente.

Definizione avanzata

La definizione si può scrivere anche su più righe, con la chiave implement:

services:
	articleFactory:
		implement: ArticleFactory

Questo formato più lungo permette di indicare argomenti aggiuntivi per il costruttore tramite la chiave arguments e ulteriori configurazioni tramite setup, come nelle normali definizioni dei servizi.

Esempio: se il metodo create() non accettasse il parametro $authorId, potremmo indicare nella configurazione un valore fisso da passare al costruttore di Article:

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

Al contrario, se create() accettasse $authorId ma questo non facesse parte del costruttore e venisse invece passato tramite un metodo come Article::setAuthorId(), faremmo riferimento al parametro nella sezione setup:

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

Accessor

Oltre alle factory, Nette sa generare anche i cosiddetti accessor. Sono oggetti con un metodo get() che restituisce un determinato servizio dal container DI. Chiamate ripetute a get() restituiscono sempre la stessa istanza.

Gli accessor offrono il caricamento pigro delle dipendenze. Immaginate una classe che registra gli errori in un database dedicato. Se questa classe ricevesse la connessione al database tramite dependency injection nel costruttore, la connessione verrebbe sempre stabilita, anche se gli errori sono rari e la connessione resta quasi sempre inutilizzata. La classe può invece ricevere un accessor. L'oggetto database (la connessione) viene creato solo quando il metodo get() dell'accessor viene chiamato la prima volta.

Come si crea un accessor? Basta scrivere un'interfaccia e Nette DI ne genererà l'implementazione. L'interfaccia deve avere esattamente un metodo chiamato get, senza parametri e con un tipo di ritorno dichiarato:

interface PDOAccessor
{
	function get(): PDO;
}

Aggiungete l'accessor al file di configurazione, insieme alla definizione del servizio che deve restituire:

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

Poiché l'accessor restituisce un servizio PDO e nella configurazione ne è definito uno solo di quel tipo, l'accessor restituirà quel servizio. Se esistessero più servizi di quel tipo, indicate per nome quale deve restituire l'accessor, per esempio - PDOAccessor(@db1).

Multifactory/accessor

Finora le nostre factory e i nostri accessor potevano creare o restituire un solo tipo di oggetto. Potete però creare facilmente delle multifactory, che uniscono le caratteristiche delle factory e degli accessor. L'interfaccia di un componente del genere può contenere più metodi chiamati create<Nome>() e get<Nome>(), per esempio:

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

Invece di iniettare più factory e accessor separati, potete quindi iniettare un unico componente più completo.

In alternativa ai metodi multipli si può usare get() con un parametro:

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

MultiFactory::getDb() fa allora la stessa cosa di MultiFactoryAlt::get('db'). Questa notazione alternativa ha però lo svantaggio che i valori supportati per $name non sono esplicitamente chiari dalla firma dell'interfaccia. Inoltre nell'interfaccia non potete definire tipi di ritorno diversi per valori diversi di $name.

Al posto di get($name) l'interfaccia può dichiarare create($name), che restituisce una nuova istanza a ogni chiamata (mentre get() ne restituisce una condivisa). L'interfaccia può contenere un solo metodo con parametro di questo tipo. Se il tipo di ritorno del metodo è nullable (per esempio ?PDO), per un $name sconosciuto restituisce null invece di sollevare un'eccezione.

Definizione con un elenco

Potete definire una multifactory nella configurazione con un elenco, scrivendo i servizi in linea:

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

In alternativa, nella definizione della multifactory potete fare riferimento a servizi esistenti tramite i riferimenti:

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

Definizione con i tag

Un altro modo di definire una multifactory è usare i tag. Il valore del tag determina il nome del metodo corrispondente:

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

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