Création d'extensions pour Nette DI

La génération du conteneur DI, en plus des fichiers de configuration, est également influencée par ce qu'on appelle des extensions. Nous les activons dans le fichier de configuration dans la section extensions.

De cette manière, nous ajoutons l'extension représentée par la classe BlogExtension sous le nom blog :

extensions:
	blog: BlogExtension

Chaque extension du compilateur hérite de Nette\DI\CompilerExtension et peut implémenter les méthodes suivantes, qui sont appelées séquentiellement lors de la construction du conteneur DI :

  1. getConfigSchema()
  2. loadConfiguration()
  3. beforeCompile()
  4. afterCompile()

getConfigSchema()

Cette méthode est appelée en premier. Elle définit le schéma pour la validation des paramètres de configuration.

Nous configurons l'extension dans la section dont le nom est le même que celui sous lequel l'extension a été ajoutée, c'est-à-dire blog :

# même nom que l'extension
blog:
	postsPerPage: 10
	allowComments: false

Nous créons un schéma décrivant toutes les options de configuration, y compris leurs types, les valeurs autorisées et éventuellement les valeurs par défaut :

use Nette\Schema\Expect;

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function getConfigSchema(): Nette\Schema\Schema
	{
		return Expect::structure([
			'postsPerPage' => Expect::int(),
			'allowComments' => Expect::bool()->default(true),
		]);
	}
}

La documentation se trouve sur la page Schéma. De plus, il est possible de spécifier quelles options peuvent être dynamiques à l'aide de dynamic(), par ex. Expect::int()->dynamic().

Nous accédons à la configuration via la variable $this->config, qui est un objet stdClass :

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		$num = $this->config->postPerPage;
		if ($this->config->allowComments) {
			// ...
		}
	}
}

loadConfiguration()

Utilisé pour ajouter des services au conteneur. Pour cela, on utilise Nette\DI\ContainerBuilder :

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		$builder = $this->getContainerBuilder();
		$builder->addDefinition($this->prefix('articles'))
			->setFactory(App\Model\HomepageArticles::class, ['@connection']) // ou setCreator()
			->addSetup('setLogger', ['@logger']);
	}
}

La convention est de préfixer les services ajoutés par l'extension avec son nom pour éviter les conflits de noms. C'est ce que fait la méthode prefix(), donc si l'extension s'appelle blog, le service portera le nom blog.articles.

Si nous devons renommer un service, nous pouvons créer un alias avec le nom d'origine pour maintenir la compatibilité ascendante. Nette fait de même, par exemple, pour le service routing.router, qui est également disponible sous son ancien nom router.

$builder->addAlias('router', 'routing.router');

Chargement des services depuis un fichier

Nous n'avons pas besoin de créer des services uniquement à l'aide de l'API de la classe ContainerBuilder, mais aussi avec la notation familière utilisée dans le fichier de configuration NEON dans la section services. Le préfixe @extension représente l'extension actuelle.

services:
	articles:
		create: MyBlog\ArticlesModel(@connection)

	comments:
		create: MyBlog\CommentsModel(@connection, @extension.articles)

	articlesList:
		create: MyBlog\Components\ArticlesList(@extension.articles)

Nous chargeons les services :

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		$builder = $this->getContainerBuilder();

		// chargement du fichier de configuration pour l'extension
		$this->compiler->loadDefinitionsFromConfig(
			$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
		);
	}
}

beforeCompile()

La méthode est appelée lorsque le conteneur contient tous les services ajoutés par les extensions individuelles dans les méthodes loadConfiguration ainsi que par les fichiers de configuration utilisateur. À ce stade de la construction, nous pouvons donc modifier les définitions de service ou ajouter des liens entre elles. Pour rechercher des services dans le conteneur par tags, on peut utiliser la méthode findByTag(), et par classe ou interface, la méthode findByType().

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function beforeCompile()
	{
		$builder = $this->getContainerBuilder();

		foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) {
			$builder->getDefinition($serviceName)->addSetup('setLogger');
		}
	}
}

afterCompile()

À ce stade, la classe du conteneur est déjà générée sous forme d'objet ClassType, contient toutes les méthodes qui créent les services et est prête à être écrite dans le cache. Nous pouvons encore modifier le code résultant de la classe à ce moment.

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function afterCompile(Nette\PhpGenerator\ClassType $class)
	{
		$method = $class->getMethod('__construct');
		// ...
	}
}

$initialization

Après la création du conteneur, la classe Configurator appelle le code d'initialisation, qui est créé en écrivant dans l'objet $this->initialization à l'aide de la méthode addBody().

Montrons un exemple de comment démarrer la session ou lancer des services qui ont le tag run avec le code d'initialisation :

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		// démarrage automatique de la session
		if ($this->config->session->autoStart) {
			$this->initialization->addBody('$this->getService("session")->start()');
		}

		// les services avec le tag run doivent être créés après l'instanciation du conteneur
		$builder = $this->getContainerBuilder();
		foreach ($builder->findByTag('run') as $name => $foo) {
			$this->initialization->addBody('$this->getService(?);', [$name]);
		}
	}
}
version: 3.x