Създаване на разширения за 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