Создание расширений для Nette DI

На генерацию DI-контейнера, помимо файлов конфигурации, влияют так называемые расширения. Мы активируем их в файле конфигурации в секции extensions.

Таким образом мы добавим расширение, представленное классом BlogExtension, под именем blog:

extensions:
	blog: BlogExtension

Каждое расширение компилятора наследуется от Nette\DI\CompilerExtension и может реализовывать следующие методы, которые последовательно вызываются во время сборки DI-контейнера:

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

getConfigSchema()

Этот метод вызывается первым. Он определяет схему для валидации конфигурационных параметров.

Расширение конфигурируется в секции, имя которой совпадает с тем, под которым было добавлено расширение, то есть blog:

# то же имя, что и у расширения
blog:
	postsPerPage: 10
	allowComments: false

Создадим схему, описывающую все опции конфигурации, включая их типы, допустимые значения и, возможно, значения по умолчанию:

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

Документацию можно найти на странице Schema. Кроме того, можно указать, какие опции могут быть динамическими с помощью dynamic(), например, Expect::int()->dynamic().

К конфигурации мы получаем доступ через переменную $this->config, которая является объектом stdClass:

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

loadConfiguration()

Используется для добавления сервисов в контейнер. Для этого служит 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']) // или setCreator()
			->addSetup('setLogger', ['@logger']);
	}
}

Конвенция заключается в том, чтобы префиксировать сервисы, добавленные расширением, его именем, чтобы избежать конфликтов имен. Это делает метод prefix(), так что если расширение называется blog, сервис будет носить имя blog.articles.

Если нам нужно переименовать сервис, мы можем для сохранения обратной совместимости создать псевдоним с исходным именем. Аналогично Nette делает, например, для сервиса routing.router, который доступен и под прежним именем router.

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

Загрузка сервисов из файла

Сервисы можно создавать не только с помощью API класса ContainerBuilder, но и знакомой записью, используемой в файле конфигурации NEON в секции services. Префикс @extension представляет текущее расширение.

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

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

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

Сервисы загрузим:

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

		// загрузка файла конфигурации для расширения
		$this->compiler->loadDefinitionsFromConfig(
			$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
		);
	}
}

beforeCompile()

Метод вызывается в момент, когда контейнер содержит все сервисы, добавленные отдельными расширениями в методах loadConfiguration, а также пользовательскими файлами конфигурации. На этом этапе сборки мы можем изменять определения сервисов или дополнять связи между ними. Для поиска сервисов в контейнере по тегам можно использовать метод findByTag(), по классу или интерфейсу — метод 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()

На этом этапе класс контейнера уже сгенерирован в виде объекта ClassType, содержит все методы, которые создают сервисы, и готов к записи в кеш. Результирующий код класса мы можем на этом этапе еще изменить.

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

$initialization

Класс Configurator после создания контейнера вызывает инициализационный код, который создается записью в объект $this->initialization с помощью метода addBody().

Покажем пример, как, например, инициализационным кодом запустить сессию или запустить сервисы, имеющие тег run:

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		// автоматический запуск сессии
		if ($this->config->session->autoStart) {
			$this->initialization->addBody('$this->getService("session")->start()');
		}

		// сервисы с тегом run должны быть созданы после инстанцирования контейнера
		$builder = $this->getContainerBuilder();
		foreach ($builder->findByTag('run') as $name => $foo) {
			$this->initialization->addBody('$this->getService(?);', [$name]);
		}
	}
}
версия: 3.x