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:
- 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']) // 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]);
}
}
}