Tworzenie rozszerzeń dla Nette DI

Oprócz plików konfiguracyjnych, na generowanie kontenera DI mają wpływ również tzw. extensions. Są one aktywowane 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ą wywoływane sekwencyjnie w miarę budowania kontenera DI:

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

getConfigSchema()

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

Rozszerzenie jest konfigurowane 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 wraz z ich typami, dozwolonymi wartościami oraz, jeśli to konieczne, wartościami domyślnymi:

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

Zobacz stronę Schemat, aby uzyskać dokumentację. Dodatkowo można określić, które opcje mogą być dynamiczne za pomocą dynamic(), np. Expect::int()->dynamic().

Dostęp do konfiguracji odbywa się poprzez 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. Aby to zrobić, należy 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']) //lub setCreator()
			->addSetup('setLogger', ['@logger']);
	}
}

Konwencją jest poprzedzanie usług dodawanych przez rozszerzenie jego nazwą, aby uniknąć konfliktów nazw. Odbywa się to za pomocą metody prefix(), więc jeśli rozszerzenie ma nazwę blog, usługa będzie miała nazwę blog.articles.

Jeśli musimy zmienić nazwę usługi, możemy stworzyć alias z oryginalną nazwą, aby zachować kompatybilność wsteczną. Nette robi podobnie na przykład w przypadku serwisu routing.router, który jest dostępny również pod dawną nazwą router.

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

Ładowanie usług z pliku

Usługi muszą być tworzone nie tylko przy użyciu API klasy ContainerBuilder, ale także przy użyciu znanej notacji stosowanej w pliku konfiguracyjnym NEON w sekcji services. Przedrostek @extension reprezentuje aktualne rozszerzenie.

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

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

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

Ładujemy usługi:

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

		// načtení konfiguračního souboru pro rozšíření
		$this->compiler->loadDefinitionsFromConfig(
			$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
		);
	}
}

beforeCompile()

Metoda jest wywoływana, gdy kontener zawiera wszystkie usługi dodane przez poszczególne rozszerzenia w metodach loadConfiguration, a także pliki konfiguracyjne użytkownika. Dzięki temu na tym etapie budowania możemy edytować definicje usług lub dodawać wiązania między nimi. Do wyszukiwania usług w kontenerze po znacznikach można użyć metody findByTag(), a po klasie lub interfejsie – 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 jako obiekt ClassType, zawiera wszystkie metody tworzące usługi i jest gotowa do zapisania w pamięci podręcznej. W tym momencie możemy jeszcze zmodyfikować wynikowy kod klasy.

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

$inicjalizacja

Po utworzeniu kontenera klasa Configurator wywołuje kod inicjalizacyjny, który powstaje poprzez zapis do obiektu $this->initialization za pomocą metody addBody().

Pokażemy przykład, jak np. kod inicjalizujący może uruchomić sesję lub uruchomić usługi, które mają znacznik run:

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

		// služby s tagem run musejí být vytvořeny po instancování kontejneru
		$builder = $this->getContainerBuilder();
		foreach ($builder->findByTag('run') as $name => $foo) {
			$this->initialization->addBody('$this->getService(?);', [$name]);
		}
	}
}
wersja: 3.x