Определение сервисов
Конфигурация — это место, где мы учим DI-контейнер, как собирать отдельные сервисы и как связывать их с другими зависимостями. Nette предоставляет очень понятный и элегантный способ достижения этой цели.
Секция services
в конфигурационном файле формата NEON — это место,
где мы определяем собственные сервисы и их конфигурации. Посмотрим на
простой пример определения сервиса с именем database
, который
представляет экземпляр класса PDO
:
services:
database: PDO('sqlite::memory:')
Указанная конфигурация приведет к следующему фабричному методу в DI-контейнере:
public function createServiceDatabase(): PDO
{
return new PDO('sqlite::memory:');
}
Имена сервисов позволяют нам ссылаться на них в других частях
конфигурационного файла в формате @имяСервиса
. Если нет
необходимости именовать сервис, мы можем просто использовать дефис:
services:
- PDO('sqlite::memory:')
Для получения сервиса из DI-контейнера мы можем использовать метод
getService()
с именем сервиса в качестве параметра или метод
getByType()
с типом сервиса:
$database = $container->getService('database');
$database = $container->getByType(PDO::class);
Создание сервиса
Обычно мы создаем сервис, просто создавая экземпляр определенного класса. Например:
services:
database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret)
Если нам нужно расширить конфигурацию дополнительными ключами, определение можно разбить на несколько строк:
services:
database:
create: PDO('sqlite::memory:')
setup: ...
Ключ create
имеет псевдоним factory
, оба варианта на практике
распространены. Однако мы рекомендуем использовать create
.
Аргументы конструктора или метода создания могут быть альтернативно
записаны в ключе arguments
:
services:
database:
create: PDO
arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret]
Сервисы не обязательно должны создаваться только простым созданием экземпляра класса, они также могут быть результатом вызова статических методов или методов других сервисов:
services:
database: DatabaseFactory::create()
router: @routerFactory::create()
Обратите внимание, что для простоты вместо ->
используется
::
, см. выразительные средства. Будут
сгенерированы следующие фабричные методы:
public function createServiceDatabase(): PDO
{
return DatabaseFactory::create();
}
public function createServiceRouter(): RouteList
{
return $this->getService('routerFactory')->create();
}
DI-контейнеру необходимо знать тип созданного сервиса. Если мы создаем сервис с помощью метода, у которого не указан тип возвращаемого значения, мы должны явно указать этот тип в конфигурации:
services:
database:
create: DatabaseFactory::create()
type: PDO
Аргументы
В конструктор и методы мы передаем аргументы способом, очень похожим на сам PHP:
services:
database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret)
Для лучшей читаемости мы можем разбить аргументы на отдельные строки. В таком случае использование запятых необязательно:
services:
database: PDO(
'mysql:host=127.0.0.1;dbname=test'
root
secret
)
Аргументы также можно именовать, и тогда не нужно беспокоиться об их порядке:
services:
database: PDO(
username: root
password: secret
dsn: 'mysql:host=127.0.0.1;dbname=test'
)
Если вы хотите пропустить некоторые аргументы и использовать их значение по умолчанию или подставить сервис с помощью autowiring, используйте подчеркивание:
services:
foo: Foo(_, %appDir%)
В качестве аргументов можно передавать сервисы, использовать параметры и многое другое, см. выразительные средства.
Setup
В секции setup
мы определяем методы, которые должны вызываться
при создании сервиса.
services:
database:
create: PDO(%dsn%, %user%, %password%)
setup:
- setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)
В PHP это выглядело бы так:
public function createServiceDatabase(): PDO
{
$service = new PDO('...', '...', '...');
$service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $service;
}
Кроме вызова методов, можно также передавать значения в свойства. Поддерживается также добавление элемента в массив, которое необходимо записывать в кавычках, чтобы не конфликтовать с синтаксисом NEON:
services:
foo:
create: Foo
setup:
- $value = 123
- '$onClick[]' = [@bar, clickHandler]
Что в PHP-коде выглядело бы следующим образом:
public function createServiceFoo(): Foo
{
$service = new Foo;
$service->value = 123;
$service->onClick[] = [$this->getService('bar'), 'clickHandler'];
return $service;
}
В setup можно также вызывать статические методы или методы других
сервисов. Если вам нужно передать в качестве аргумента текущий сервис,
укажите его как @self
:
services:
foo:
create: Foo
setup:
- My\Helpers::initializeFoo(@self)
- @anotherService::setFoo(@self)
Обратите внимание, что для простоты вместо ->
используется
::
, см. выразительные средства. Будет
сгенерирован такой фабричный метод:
public function createServiceFoo(): Foo
{
$service = new Foo;
My\Helpers::initializeFoo($service);
$this->getService('anotherService')->setFoo($service);
return $service;
}
Выразительные средства
Nette DI предоставляет нам чрезвычайно богатые выразительные средства, с помощью которых мы можем записать почти все что угодно. В конфигурационных файлах мы можем использовать параметры:
# параметр
%wwwDir%
# значение параметра под ключом
%mailer.user%
# параметр внутри строки
'%wwwDir%/images'
Далее создавать объекты, вызывать методы и функции:
# создание объекта
DateTime()
# вызов статического метода
Collator::create(%locale%)
# вызов PHP функции
::getenv(DB_USER)
Ссылаться на сервисы либо по их имени, либо по типу:
# сервис по имени
@database
# сервис по типу
@Nette\Database\Connection
Использовать синтаксис first-class callable:
# создание callback, аналог [@user, logout]
@user::logout(...)
Использовать константы:
# константа класса
FilesystemIterator::SKIP_DOTS
# глобальную константу получим PHP функцией constant()
::constant(PHP_VERSION)
Вызовы методов можно объединять в цепочку так же, как в PHP. Только для
простоты вместо ->
используется ::
:
DateTime()::format('Y-m-d')
# PHP: (new DateTime())->format('Y-m-d')
@http.request::getUrl()::getHost()
# PHP: $this->getService('http.request')->getUrl()->getHost()
Эти выражения можно использовать где угодно, при создании сервисов, в аргументах, в секции setup или параметрах:
parameters:
ipAddress: @http.request::getRemoteAddress()
services:
database:
create: DatabaseFactory::create( @anotherService::getDsn() )
setup:
- initialize( ::getenv('DB_USER') )
Специальные функции
В конфигурационных файлах вы можете использовать эти специальные функции:
not()
отрицание значенияbool()
,int()
,float()
,string()
преобразование типа без потерьtyped()
создает массив всех сервисов указанного типаtagged()
создает массив всех сервисов с данным тегом
services:
- Foo(
id: int(::getenv('ProjectId'))
productionMode: not(%debugMode%)
)
В отличие от классического приведения типов в PHP, такого как
(int)
, преобразование без потерь вызовет исключение для
нечисловых значений.
Функция typed()
создает массив всех сервисов данного типа (класс
или интерфейс). Она пропускает сервисы, у которых отключен autowiring. Можно
указать несколько типов, разделенных запятой.
services:
- BarsDependent( typed(Bar) )
Массив сервисов определенного типа можно передавать как аргумент также автоматически с помощью autowiring.
Функция tagged()
создает массив всех сервисов с определенным
тегом. Здесь также можно указать несколько тегов, разделенных
запятой.
services:
- LoggersDependent( tagged(logger) )
Autowiring
Ключ autowired
позволяет влиять на поведение autowiring для
конкретного сервиса. Для деталей см. главу об autowiring.
services:
foo:
create: Foo
autowired: false # сервис foo исключен из autowiring
Ленивые сервисы
Ленивая загрузка (Lazy loading) — это техника, которая откладывает создание сервиса до момента, когда он действительно необходим. В глобальной конфигурации можно включить ленивое создание для всех сервисов сразу. Для отдельных сервисов можно переопределить это поведение:
services:
foo:
create: Foo
lazy: false
Когда сервис определен как ленивый, при его запросе из DI-контейнера мы получаем специальный объект-заместитель. Он выглядит и ведет себя так же, как реальный сервис, но фактическая инициализация (вызов конструктора и setup) происходит только при первом вызове любого его метода или свойства.
Ленивая загрузка может использоваться только для пользовательских классов, а не для внутренних классов PHP. Требуется PHP 8.4 или новее.
Теги
Теги служат для добавления дополнительной информации к сервисам. Сервису можно добавить один или несколько тегов:
services:
foo:
create: Foo
tags:
- cached
Теги также могут нести значения:
services:
foo:
create: Foo
tags:
logger: monolog.logger.event
Чтобы получить все сервисы с определенными тегами, вы можете
использовать функцию tagged()
:
services:
- LoggersDependent( tagged(logger) )
В DI-контейнере вы можете получить имена всех сервисов с определенным
тегом с помощью метода findByTag()
:
$names = $container->findByTag('logger');
// $names - это массив, содержащий имя сервиса и значение тега
// например, ['foo' => 'monolog.logger.event', ...]
Режим Inject
С помощью флага inject: true
активируется передача зависимостей
через публичные переменные с аннотацией inject и методы inject*().
services:
articles:
create: App\Model\Articles
inject: true
По умолчанию inject
активирован только для презентеров.
Модификация сервисов
DI-контейнер содержит множество сервисов, которые были добавлены с
помощью встроенного или пользовательского
расширения. Вы можете изменять определения этих сервисов прямо в
конфигурации. Например, вы можете изменить класс сервиса
application.application
, который по умолчанию является
Nette\Application\Application
, на другой:
services:
application.application:
create: MyApplication
alteration: true
Флаг alteration
является информативным и указывает, что мы только
модифицируем существующий сервис.
Мы также можем дополнить setup:
services:
application.application:
create: MyApplication
alteration: true
setup:
- '$onStartup[]' = [@resource, init]
При переопределении сервиса мы можем захотеть удалить исходные
аргументы, элементы setup или теги, для чего служит reset
:
services:
application.application:
create: MyApplication
alteration: true
reset:
- arguments
- setup
- tags
Если вы хотите удалить сервис, добавленный расширением, вы можете сделать это так:
services:
cache.journal: false