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