Tworzenie rozszerzeń dla Nette DI
Na 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:
- getConfigSchema()
- loadConfiguration()
- beforeCompile()
- 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']) // lub 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]);
}
}
}