Створення розширень для Nette DI
Генерацію DI-контейнера, крім конфігураційних файлів,
впливають також так звані розширення. Ми активуємо їх у
конфігураційному файлі в секції extensions
.
Так ми додаємо розширення, представлене класом BlogExtension
, під
назвою blog
:
extensions:
blog: BlogExtension
Кожне розширення компілятора успадковує від Nette\DI\CompilerExtension і може реалізовувати наступні методи, які послідовно викликаються під час складання DI-контейнера:
- getConfigSchema()
- loadConfiguration()
- beforeCompile()
- afterCompile()
getConfigSchema()
Цей метод викликається першим. Він визначає схему для валідації конфігураційних параметрів.
Розширення конфігуруємо в секції, назва якої збігається з тією, під
якою було додано розширення, тобто blog
:
# та сама назва, що й у розширення
blog:
postsPerPage: 10
allowComments: false
Створимо схему, що описує всі конфігураційні опції, включаючи їхні типи, допустимі значення та, можливо, значення за замовчуванням:
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),
]);
}
}
Документацію знайдете на сторінці Schema. Крім
того, можна визначити, які опції можуть бути динамічними за допомогою
dynamic()
, наприклад Expect::int()->dynamic()
.
До конфігурації ми отримуємо доступ через змінну $this->config
,
яка є об'єктом stdClass
:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
$num = $this->config->postPerPage;
if ($this->config->allowComments) {
// ...
}
}
}
loadConfiguration()
Використовується для додавання сервісів до контейнера. Для цього служить 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']);
}
}
Конвенція полягає в тому, щоб префіксувати сервіси, додані
розширенням, його назвою, щоб уникнути конфліктів імен. Це робить метод
prefix()
, тому якщо розширення називається blog
, сервіс матиме
назву blog.articles
.
Якщо потрібно перейменувати сервіс, для збереження зворотної
сумісності можна створити псевдонім з оригінальною назвою. Подібно Nette
робить, наприклад, для сервісу routing.router
, який доступний і під
попередньою назвою router
.
$builder->addAlias('router', 'routing.router');
Завантаження сервісів з файлу
Сервіси можна створювати не лише за допомогою API класу ContainerBuilder, але й
відомим записом, що використовується в конфігураційному файлі NEON у
секції services. Префікс @extension
представляє поточне розширення.
services:
articles:
create: MyBlog\ArticlesModel(@connection)
comments:
create: MyBlog\CommentsModel(@connection, @extension.articles)
articlesList:
create: MyBlog\Components\ArticlesList(@extension.articles)
Завантажимо сервіси:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
$builder = $this->getContainerBuilder();
// завантаження конфігураційного файлу для розширення
$this->compiler->loadDefinitionsFromConfig(
$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
);
}
}
beforeCompile()
Метод викликається в момент, коли контейнер містить усі сервіси,
додані окремими розширеннями в методах loadConfiguration
, а також
користувацькими конфігураційними файлами. На цій стадії складання ми
можемо редагувати визначення сервісів або доповнювати зв'язки між
ними. Для пошуку сервісів у контейнері за тегами можна використовувати
метод findByTag()
, а за класом чи інтерфейсом – метод
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()
На цій фазі клас контейнера вже згенеровано у вигляді об'єкта ClassType, він містить усі методи, що створюють сервіси, і готовий до запису в кеш. Кінцевий код класу ми можемо на цьому етапі ще змінити.
class BlogExtension extends Nette\DI\CompilerExtension
{
public function afterCompile(Nette\PhpGenerator\ClassType $class)
{
$method = $class->getMethod('__construct');
// ...
}
}
$initialization
Клас Configurator після створення
контейнера викликає ініціалізаційний код, який створюється записом
в об'єкт $this->initialization
за допомогою методу addBody().
Покажемо приклад, як, наприклад, ініціалізаційним кодом запустити
сесію або запустити сервіси, що мають тег run
:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
// автоматичний запуск сесії
if ($this->config->session->autoStart) {
$this->initialization->addBody('$this->getService("session")->start()');
}
// сервіси з тегом run мають бути створені після інстанціювання контейнера
$builder = $this->getContainerBuilder();
foreach ($builder->findByTag('run') as $name => $foo) {
$this->initialization->addBody('$this->getService(?);', [$name]);
}
}
}