Criação de extensões para Nette DI

A geração do contêiner DI, além dos arquivos de configuração, também é influenciada pelas chamadas extensões. Nós as ativamos no arquivo de configuração na seção extensions.

Desta forma, adicionamos a extensão representada pela classe BlogExtension sob o nome blog:

extensions:
	blog: BlogExtension

Cada extensão do compilador herda de Nette\DI\CompilerExtension e pode implementar os seguintes métodos, que são chamados sequencialmente durante a construção do contêiner DI:

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

getConfigSchema()

Este método é chamado primeiro. Ele define o schema para validação dos parâmetros de configuração.

Configuramos a extensão na seção cujo nome é o mesmo sob o qual a extensão foi adicionada, ou seja, blog:

# mesmo nome da extensão
blog:
	postsPerPage: 10
	allowComments: false

Criamos um schema descrevendo todas as opções de configuração, incluindo seus tipos, valores permitidos e, opcionalmente, valores padrão:

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),
		]);
	}
}

A documentação pode ser encontrada na página Schema. Além disso, pode-se especificar quais opções podem ser dinâmicas usando dynamic(), např. Expect::int()->dynamic().

Acessamos a configuração através da variável $this->config, que é um objeto stdClass:

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

loadConfiguration()

Usado para adicionar serviços ao contêiner. Para isso, serve a 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']);
	}
}

A convenção é prefixar os serviços adicionados pela extensão com seu nome, para que não ocorram conflitos de nomes. O método prefix() faz isso, então se a extensão se chama blog, o serviço será nomeado blog.articles.

Se precisarmos renomear um serviço, podemos, para manter a compatibilidade retroativa, criar um alias com o nome original. A Nette faz algo semelhante, por exemplo, com o serviço routing.router, que também está disponível sob o nome anterior router.

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

Carregamento de serviços de um arquivo

Não precisamos criar serviços apenas usando a API da classe ContainerBuilder, mas também com a notação familiar usada no arquivo de configuração NEON na seção services. O prefixo @extension representa a extensão atual.

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

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

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

Carregamos os serviços:

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

		// carregamento do arquivo de configuração para a extensão
		$this->compiler->loadDefinitionsFromConfig(
			$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
		);
	}
}

beforeCompile()

O método é chamado no momento em que o contêiner contém todos os serviços adicionados pelas extensões individuais nos métodos loadConfiguration e também pelos arquivos de configuração do usuário. Nesta fase de construção, podemos, portanto, modificar as definições de serviço ou adicionar ligações entre eles. Para pesquisar serviços no contêiner por tags, pode-se usar o método findByTag(), por classe ou interface, o método 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()

Nesta fase, a classe do contêiner já está gerada na forma de um objeto ClassType, contém todos os métodos que criam serviços e está pronta para ser escrita no cache. O código resultante da classe ainda pode ser modificado neste momento.

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

$initialization

A classe Configurator, após criar o contêiner, chama o código de inicialização, que é criado escrevendo no objeto $this->initialization usando o método addBody().

Mostraremos um exemplo de como, por exemplo, iniciar a sessão com o código de inicialização ou iniciar serviços que têm a tag run:

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		// início automático da sessão
		if ($this->config->session->autoStart) {
			$this->initialization->addBody('$this->getService("session")->start()');
		}

		// serviços com a tag run devem ser criados após a instanciação do contêiner
		$builder = $this->getContainerBuilder();
		foreach ($builder->findByTag('run') as $name => $foo) {
			$this->initialization->addBody('$this->getService(?);', [$name]);
		}
	}
}
versão: 3.x