Tworzenie rozszerzeń dla Nette DI

Generowanie kontenera DI oprócz plików konfiguracyjnych wpływają również tzw. rozszerzenia. Aktywujemy je w pliku konfiguracyjnym w sekcji extensions.

W ten sposób dodajemy rozszerzenie reprezentowane przez klasę BlogExtension pod nazwą blog:

extensions:
	blog: BlogExtension

Każde rozszerzenie kompilatora dziedziczy po Nette\DI\CompilerExtension i może implementować następujące metody, które są kolejno wywoływane podczas budowania kontenera DI:

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

getConfigSchema()

Ta metoda jest wywoływana jako pierwsza. Definiuje schemat do walidacji parametrów konfiguracyjnych.

Rozszerzenie konfigurujemy w sekcji, której nazwa jest taka sama jak ta, pod którą rozszerzenie zostało dodane, czyli blog:

# ta sama nazwa co rozszerzenie
blog:
	postsPerPage: 10
	allowComments: false

Tworzymy schemat opisujący wszystkie opcje konfiguracyjne, w tym ich typy, dozwolone wartości i ewentualnie wartości domyślne:

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

Dokumentację znajdziesz na stronie Schema. Dodatkowo można określić, które opcje mogą być dynamiczne za pomocą dynamic(), np. Expect::int()->dynamic().

Do konfiguracji dostajemy się przez zmienną $this->config, która jest obiektem stdClass:

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

loadConfiguration()

Służy do dodawania usług do kontenera. Do tego służy 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']);
	}
}

Konwencją jest prefiksowanie usług dodanych przez rozszerzenie jego nazwą, aby nie dochodziło do konfliktów nazw. Robi to metoda prefix(), więc jeśli rozszerzenie nazywa się blog, usługa będzie nosić nazwę blog.articles.

Jeśli potrzebujemy zmienić nazwę usługi, możemy ze względu na zachowanie wstecznej kompatybilności utworzyć alias z pierwotną nazwą. Podobnie robi Nette np. w przypadku usługi routing.router, która jest dostępna również pod wcześniejszą nazwą router.

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

Ładowanie usług z pliku

Usługi możemy tworzyć nie tylko za pomocą API klasy ContainerBuilder, ale także znanym zapisem używanym w pliku konfiguracyjnym NEON w sekcji services. Prefiks @extension reprezentuje bieżące rozszerzenie.

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

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

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

Usługi wczytamy:

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

		// wczytanie pliku konfiguracyjnego dla rozszerzenia
		$this->compiler->loadDefinitionsFromConfig(
			$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
		);
	}
}

beforeCompile()

Metoda jest wywoływana w momencie, gdy kontener zawiera wszystkie usługi dodane przez poszczególne rozszerzenia w metodach loadConfiguration oraz przez użytkownika w plikach konfiguracyjnych. Na tym etapie budowania możemy więc modyfikować definicje usług lub uzupełniać powiązania między nimi. Do wyszukiwania usług w kontenerze według tagów można użyć metody findByTag(), a według klasy lub interfejsu metody 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()

Na tym etapie klasa kontenera jest już wygenerowana w postaci obiektu ClassType, zawiera wszystkie metody tworzące usługi i jest gotowa do zapisu do cache. Wynikowy kod klasy możemy na tym etapie jeszcze zmodyfikować.

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

$initialization

Klasa Configurator po utworzeniu kontenera wywołuje kod inicjalizacyjny, który tworzy się zapisem do obiektu $this->initialization za pomocą metody addBody().

Pokażemy przykład, jak na przykład kodem inicjalizacyjnym uruchomić sesję lub uruchomić usługi, które mają tag run:

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		// automatyczne uruchamianie sesji
		if ($this->config->session->autoStart) {
			$this->initialization->addBody('$this->getService("session")->start()');
		}

		// usługi z tagiem run muszą być utworzone po instancjonowaniu kontenera
		$builder = $this->getContainerBuilder();
		foreach ($builder->findByTag('run') as $name => $foo) {
			$this->initialization->addBody('$this->getService(?);', [$name]);
		}
	}
}
wersja: 3.x