Создание расширений для 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']) // или 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]);
}
}
}