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

Конфигурация — это место, где мы учим 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
версия: 3.x