Factories générées

Nette DI sait générer automatiquement le code des factories à partir d'interfaces, ce qui vous évite d'écrire du code.

Une factory est une classe chargée de créer des objets et de leur passer leurs dépendances. Ne la confondez pas avec le patron de conception factory method, qui décrit une façon particulière d'utiliser les factories et n'a rien à voir avec ce sujet.

Nous avons montré à quoi ressemble une telle factory dans le chapitre d'introduction :

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

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

Nette DI sait générer automatiquement le code d'une factory. Il vous suffit de créer une interface et Nette DI en générera l'implémentation. L'interface doit avoir exactement une méthode nommée create et déclarer un type de retour :

interface ArticleFactory
{
	function create(): Article;
}

Ainsi, la factory ArticleFactory possède une méthode create qui crée des objets Article. La classe Article peut par exemple ressembler à ceci :

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

Ajoutez la factory au fichier de configuration :

services:
	- ArticleFactory

Nette DI générera l'implémentation correspondante de la factory.

Dans le code qui utilise la factory, demandez l'objet par son interface et Nette DI vous fournira l'implémentation générée :

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

	public function foo()
	{
		// laissons la factory créer un objet
		$article = $this->articleFactory->create();
	}
}

Factory paramétrée

La méthode create de la factory peut accepter des paramètres, qu'elle passe ensuite au constructeur. Ajoutons par exemple l'identifiant de l'auteur de l'article à la classe Article :

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

Nous ajoutons également le paramètre à la factory :

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

Comme le nom du paramètre dans le constructeur ($authorId) correspond au nom du paramètre de la méthode de la factory, Nette DI le passe automatiquement.

Définition avancée

La définition peut aussi s'écrire sous forme multiligne à l'aide de la clé implement :

services:
	articleFactory:
		implement: ArticleFactory

Ce format plus long permet d'indiquer des arguments supplémentaires pour le constructeur via la clé arguments et de poursuivre la configuration avec setup, comme pour les définitions de services ordinaires.

Exemple : si la méthode create() n'acceptait pas le paramètre $authorId, nous pourrions fournir dans la configuration une valeur fixe à passer au constructeur d'Article :

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

À l'inverse, si create() acceptait $authorId mais que celui-ci ne faisait pas partie du constructeur et était passé par une méthode comme Article::setAuthorId(), nous référencerions le paramètre dans la section setup :

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

Accessor

Outre les factories, Nette sait aussi générer ce qu'on appelle des accesseurs. Ce sont des objets dotés d'une méthode get() qui renvoie un service précis du conteneur DI. Les appels répétés à get() renvoient toujours la même instance.

Les accesseurs assurent le chargement paresseux des dépendances. Imaginez une classe qui journalise les erreurs dans une base de données dédiée. Si cette classe recevait la connexion à la base de données par injection dans le constructeur, la connexion serait toujours établie, même si les erreurs sont rares et que la connexion reste inutilisée la plupart du temps. À la place, la classe peut recevoir un accesseur. L'objet de base de données (la connexion) n'est créé qu'au premier appel de la méthode get() de l'accesseur.

Comment créer un accesseur ? Écrivez simplement une interface et Nette DI en générera l'implémentation. L'interface doit avoir exactement une méthode nommée get, sans paramètre, et déclarer le type de retour :

interface PDOAccessor
{
	function get(): PDO;
}

Ajoutez l'accesseur au fichier de configuration, avec la définition du service qu'il doit renvoyer :

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

Comme l'accesseur renvoie un service PDO et qu'un seul service de ce genre est défini dans la configuration, l'accesseur renverra ce service précis. Si plusieurs services de ce type existent, indiquez par son nom celui que l'accesseur doit renvoyer, par exemple - PDOAccessor(@db1).

Multifactory/Accessor

Jusqu'ici, nos factories et accesseurs ne pouvaient créer ou renvoyer qu'un seul type d'objet. Vous pouvez cependant créer facilement des multifactories, qui combinent les propriétés des factories et des accesseurs. L'interface d'un tel composant peut contenir plusieurs méthodes nommées create<Nom>() et get<Nom>(), par exemple :

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

Ainsi, au lieu d'injecter plusieurs factories et accesseurs distincts, vous pouvez injecter un seul composant plus complet.

Autre possibilité : au lieu de plusieurs méthodes, utiliser get() avec un paramètre :

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

Alors MultiFactory::getDb() fait la même chose que MultiFactoryAlt::get('db'). Cette notation alternative a toutefois l'inconvénient que les valeurs acceptées pour $name ne ressortent pas explicitement de la signature de l'interface. De plus, vous ne pouvez pas définir dans l'interface des types de retour différents selon la valeur de $name.

Au lieu de get($name), l'interface peut déclarer create($name), qui renvoie une nouvelle instance à chaque appel (alors que get() en renvoie une partagée). L'interface ne peut contenir qu'une seule méthode paramétrée de ce genre. Si le type de retour de la méthode est nullable (par exemple ?PDO), elle renvoie null pour un $name inconnu au lieu de lever une exception.

Définition à l'aide d'une liste

Vous pouvez définir une multifactory dans la configuration à l'aide d'une liste, les services étant écrits directement dedans :

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

Vous pouvez aussi renvoyer, dans la définition de la multifactory, vers des services existants à l'aide de références :

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

Définition à l'aide de tags

Une autre façon de définir une multifactory est d'utiliser les tags. La valeur du tag détermine le nom de la méthode correspondante :

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

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