Створення розширень для 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']) // or 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