DI: tvorba rozšíření

Generování DI kontejneru kromě konfiguračních souborů ovlivňují ještě tzv rozšíření. Aktivujeme je v konfiguračním souboru v sekci extensions.

Takto přidáme rozšíření reprezentované třídou BlogExtension pod názvem blog:

extensions:
	blog: BlogExtension

Každé rozšíření kompileru dědí od Nette\DI\CompilerExtension a může implementovat následující metody, které jsou postupně volány během sestavování DI kontejneru:

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

getConfigSchema()

Tato metoda se volá jako první. Definuje schema pro validaci konfiguračních parametrů.

Rozšíření konfigurujeme v sekci, jejíž název je stejný jako ten, pod kterým bylo rozšíření přidáno, tedy blog:

# stejné jméno jako má extension
blog:
	postsPerPage: 10
	allowComments: false

Vytvoříme schema popisující všechny konfigurační volby včetně jejich typů, povolených hodnot a případně i výchozích hodnot:

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

Dokumentaci najdete na stránce Schema. Navíc lze určit, které volby mohou být dynamické pomocí dynamic(), např. Expect::int()->dynamic().

Ke konfiguraci se dostaneme přes proměnnou $this->config, což je objekt stdClass:

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

loadConfiguration()

Používá se přidání služeb do kontejneru. K tomu slouží 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'])
			->addSetup('setLogger', ['@logger']);
	}
}

Konvence je prefixovat služby přidané rozšířením jeho názvem, aby nevznikaly jmenné konflikty. To dělá metoda prefix(), takže pokud se rozšíření jmenuje blog, služba ponese název blog.articles.

Pokud potřebujeme přejmenovat službu, můžeme kvůli zachování zpětné kompatibility vytvořit alias s původním názvem. Podobně to dělá Nette např. u služby routing.router, která je dostupná i pod dřívějším názvem router.

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

Načtení služeb ze souboru

Služby nemusíme vytvářet jen pomocí API třídy ContainerBuilder, ale i známým zápisem používaným v konfiguračním souboru NEON v sekci services. Prefix @extension představuje aktuální extension.

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

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

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

Služby načteme:

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 se volá ve chvíli, kdy kontejner obsahuje všechny služby přidané jednotlivými rozšířeními v metodách loadConfiguration a taktéž uživatelskými konfiguračními soubory. V této fázi sestavování tedy můžeme definice služeb upravovat nebo doplnit vazby mezi nimi. Pro vyhledávání služeb v kontejneru podle tagů lze využít metodu findByTag(), podle třídy či rozhraní zase metodu findByType().

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

		foreach ($builder->findByTag('logaware') as $name) {
			$builder->getDefinition($name)->addSetup('setLogger');
		}
	}
}

afterCompile()

V této fázi už je třída kontejneru vygenerována v podobě objektu ClassType, obsahuje všechny metody, které vytváří služby, a je připravena na zápis do cache. Výsledný kód třídy můžeme v této chvíli ještě upravit.

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

$initialization

Třída Configurator po vytvoření kontejneru volá inicializační kód, který se vytváří zápisem do objektu $this->initialization pomocí metody addBody().

Ukážeme si příklad, jak třeba inicializačním kódem nastartovat session nebo spustit služby, které mají tag run:

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		// automatické startování session
		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]);
		}
	}
}

Související články na blogu