Определение сервисов

В конфигурации мы указываем 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
версия: 3.x