Определение сервисов
В конфигурации мы указываем 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)
Обращаться к публичным свойствам и константам сервиса через
@service::member. Является ли имя свойством или константой, решает его
первая буква: строчная означает публичное свойство, заглавная –
константу:
# публичное свойство сервиса (начинается со строчной буквы)
@settings::apiUrl
# константа класса сервиса (начинается с заглавной буквы)
@settings::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
Ленивые сервисы
Ленивая загрузка – приём, откладывающий создание сервиса до момента, когда он действительно понадобится. В глобальной конфигурации вы можете включить ленивое создание сразу для всех сервисов. Для отдельных сервисов это поведение можно переопределить:
services:
foo:
create: Foo
lazy: false
Когда сервис определён как ленивый, при запросе из DI-контейнера мы получаем специальный объект-заместитель. Этот заместитель выглядит и ведёт себя точно как настоящий сервис, но настоящая инициализация (вызов конструктора и настроек) происходит только при первом обращении к какому-либо из его методов или свойств.
Учтите, что раз сервис создаётся позже, то и ошибки в его конфигурации проявляются позже. Например, неверные учётные данные базы данных обнаружатся не при старте приложения, а только при первом запросе.
Ленивое создание к тому же смягчает круговые зависимости, то есть
ситуацию, когда сервис A требует сервис B, а B одновременно требует A. Без
него контейнер сообщает об ошибке Circular reference detected. С ленивым
заместителем сервис A получает лишь заместителя сервиса B, который
инициализируется тогда, когда действительно используется, в момент,
когда A уже существует. Тем не менее круговая зависимость
сигнализирует об ошибочной архитектуре, и от неё лучше избавиться.
Ленивая загрузка требует PHP 8.4 или новее и работает только для
сервисов, создаваемых прямым созданием экземпляра класса (например,
create: Foo), но не для тех, что создаются фабричным методом. Её также
нельзя использовать для классов, которые в конечном счёте наследуют от
внутреннего класса PHP. Когда ленивую загрузку применить нельзя, флаг
lazy: true молча игнорируется.
Теги
Теги служат для добавления к сервисам дополнительных сведений. Сервису можно назначить один или несколько тегов:
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]
Определять сервис по его внутреннему имени необязательно, вместо этого можно сослаться на него по типу. Предыдущий пример можно записать и так:
services:
@Nette\Application\Application:
create: MyApplication
Изменяя сервис, мы можем захотеть убрать исходные аргументы,
элементы setup или теги с помощью ключа reset:
services:
application.application:
create: MyApplication
alteration: true
reset:
arguments: true
setup: true
tags: true
Если вы хотите удалить сервис, добавленный расширением, это делается так:
services:
cache.journal: false