Новое в Symfony 6.3 — Улучшения Dependency Injection

Источник: «New in Symfony 6.3: Dependency Injection Improvements»
В Symfony 6.3 улучшен компонент Dependency Injection, добавлена поддержка автоподключения сервисов в виде замыканий и вызываемых методов, генерация адаптеров для функциональных интерфейсов.

Сервис Контейнер — ключевая функция делающая приложение Symfony таким быстрым и гибким. В Symfony 6.3 мы улучшили его, добавив множество новых возможностей.

Новые опции атрибута Autowire

Атрибут Autowire был введён в Symfony 6.1 и позволяет автоматически подключать сервисы, параметры и выражения. В Symfony 6.3 он также может автоматически связывать переменные среды (через опцию env). Кроме того, теперь параметры автоматически подключаются с помощью новой опции param:

// src/Service/MessageGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MessageGenerator
{
public function __construct(
// ...

// при использовании 'param' вам не нужно оборачивать имя параметра в '%'
#[Autowire(param: 'kernel.debug')]
bool $debugMode,

#[Autowire(env: 'SOME_ENV_VAR')]
string $senderName,
) {
}
// ...
}

Настройка псевдонимов с атрибутами

Сервис псевдонимов позволяет использовать сервисы с использованием собственного идентификатора сервиса вместо исходного идентификатора, присвоенного сервису. В Symfony 6.3 мы добавляем новый атрибут #[AsAlias], чтобы вы могли задать псевдоним прямо в коде:

// src/Mail/PhpMailer.php
namespace App\Mail;

// ...
use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias(id: 'app.mailer', public: true)]
class PhpMailer
{
// ...
}

Когда используется атрибут #[AsAlias] вы можете не передавать аргумент id, если класс сервиса реализует ровно один интерфейс. В этих случаях в качестве псевдонима будет использоваться FQCN интерфейса.

Новы опции для атрибута Autoconfigure

При использовании класс в качестве собственной фабрики сервисов вы можете использовать новую опцию конструктора #[Autoconfigure] для определения имени метода класса, который действует как его конструктор:

// src/Email/NewsletterManager.php
namespace App\Email;

// ...
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(constructor: 'create')]
class NewsletterManager
{
private string $sender;

public static function create(
#[Autowire(param: 'app.newsletter_sender')] string $sender
): self
{
$newsletterManager = new self();
$newsletterManager->sender = $sender;
// ...

return $newsletterManager;
}

// ...
}

Вложение связанных атрибутов в Autowire

В Symfony 6.3 мы улучшили атрибут #[Autowire], чтобы вы могли вкладывать в него другие атрибуты, связанные с автоматическим подключением. В следующем примере показана эта функция в действии со всеми доступными параметрами:

#[AsDecorator(decorates: AsDecoratorFoo::class)]
class AutowireNestedAttributes implements AsDecoratorInterface
{
public function __construct(
#[Autowire([
'decorated' => new MapDecorated(),
'iterator' => new TaggedIterator('foo'),
'locator' => new TaggedLocator('foo'),
'service' => new Autowire(service: 'bar')
])
]
array $options)
{
}
}

Использование Autowire для генерации замыканий

В компоненте Dependency Injection, замыкания сервиса — это замыкания, которые возвращают сервис. Они пригодятся, когда имеешь дело с ленью со стороны потребителя. В Symfony 6.3 мы добавили поддержку автоподключения сервисов, как замыканий с помощью атрибута #[AutowireServiceClosure]:

#[AutowireServiceClosure('my_service')]
Closure $serviceResolver

Это сгенерирует замыкание, которое возвращает сервис my_service при вызове.

Также довольно часто служба принимает замыкание с определённой подписью в качестве аргумента. В Symfony 6.3 вы можете генерировать такие замыкания используя атрибут #[AutowireCallable]:

#[AutowireCallable(service: 'my_service', method: 'myMethod')]
Closure $callable

Это создаст замыкание, которое будет иметь ту же сигнатуру, что и метод, так что его вызов будет перенаправлен на myMethod() в сервисе my_service. Этот тип внедрения замыкания по умолчанию не является ленивым: экземпляр my_service будет создан при создании аргумент $callable. Если вы хотите сделать его ленивым, вы можете использовать опцию lazy:

#[AutowireCallable(service: 'my_service', method: 'myMethod', lazy: true)]
Closure $callable

Это создаст замыкание, которое создаст экземпляр сервиса my_service только при вызове замыкания.

Генерация адаптеров для функциональных интерфейсов

Функциональные интерфейсы — это интерфейсы с одним методом. Концептуально они похожи на замыкания, за исключением того, что их единственный метод имеет имя, и их можно использовать как подсказки типа.

Атрибут #[AutowireCallable] можно использовать в качестве адаптера для функционального интерфейса. Например, если у вас есть следующий функциональный интерфейс:

interface UriExpanderInterface
{
public function expand(string $uri, array $parameters): string;
}

Вы можете использовать атрибут #[AutowireCallable] для создания адаптера для него:

#[AutowireCallable(service: 'my_service', method: 'myMethod')]
UriExpanderInterface $expander

Даже если my_service не реализует UriExpanderInterface, аргумент $expander будет экземпляром UriExpanderInterface сгенерированным Symfony. Вызов его метода expand() будет направлен на метод myMethod() сервиса my_service.

Также предусмотрена поддержка для создания таких адаптеров в YAML, XML или PHP.

Автоматическое связывание ленивых сервисов

Атрибут #[Autowire] можно использовать, чтобы указать, как сервисы должны быть автоматически подключены. В Symfony 6.3 мы добавили поддержку автоматического связывания ленивых сервисов с помощью опции lazy:

#[Autowire(lazy: true)]
MyServiceInterface $service

Это сгенерирует ленивый сервис, который будет создан только тогда, когда аргумент $service фактически используется. Это работает путём генерации прокси-класса, который реализует MyServiceInterface и перенаправляет все вызовы методов в реальный сервис. При нацеливании аргумента со многими возможными типами, вы можете использовать опцию lazy со значением строки класса, чтобы указать, какой тип должен быть сгенерирован:

#[Autowire(lazy: FirstServiceInterface::class)]
FirstServiceInterface|SecondServiceInterface $service

Это сгенерирует прокси-класс реализующий только FirstServiceInterface.

Эта возможность позволяет сервисам, которые не всегда используют свои зависимости, инициализировать их только тогда, когда это действительно необходимо. Это может обеспечить значительный прирост производительности, особенно для сервисов, инициализация которых требует больших затрат.

Устаревшие параметры контейнера

Ещё одна новая функция связанная с Сервис Контейнером, заключается в том, что теперь вы можете объявлять параметры устаревшими. Это полезно, например, для расширений, в которых хотят переименовать или переименовать параметры, чтобы предупредить пользователей об этом будущем изменении, прежде чем применить его.

public function load(array $configs, ContainerBuilder $containerBuilder)
{
// ...

$containerBuilder->setParameter('acme_demo.database_user', $configs['db_user']);

// устаревший параметр должен быть установлен до того, как он будет помечен устаревшим
$containerBuilder->deprecateParameter(
'acme_demo.database_user',
'acme/database-package',
'1.3',
// при желании вы можете установить собственное сообщение об устаревании
'"acme_demo.database_user" is deprecated, you should configure database credentials with the "acme_demo.database_dsn" parameter instead.'
);
}

Рассмотрите возможность использования этой опции, если вы хотите преобразовать ваши временные параметры в параметры сборки, ещё одну новую функцию, представленную в Symfony 6.3.

Исключение классов с атрибутами

При конфигурировании сервисов в контейнере, вы можете использовать опцию exclude, чтобы указать Symfony не создавать сервисы для одного или нескольких классов. Этот параметр удобен при исключении целых каталогов (например, src/Entity/). Однако, если вы просто хотите исключить некоторые определённые классы, в Symfony 6.3 вы также можете сделать это с помощью нового атрибута #[Exclude]:

// src/Kernel.php
namespace App;

use Symfony\Component\Dependency\Injection\Attribute\Exclude;

#[Exclude]
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}

Разрешение расширения атрибута Autowire

В Symfony 6.3 атрибут #[Autowire] может быть расширен для создания ваших хелперов автопроводки. Например, рассмотрите этот пример, который создаёт настраиваемый атрибут для репозиториев autowire:

use Symfony\Component\DependencyInjection\Attribute\Autowire;

class Repository extends Autowire
{
public function __construct(string $class)
{
parent::__construct(expression: \sprintf("service('some.repository.factory').create('%s')", $class));
}
}

А затем используйте его в своём проекте следующим образом:

/**
* @param ObjectRepository<User> $repository
*/

public function __construct(
#[Repository(User::class)] private ObjectRepository $repository
) {
}

Спасибо всем участникам улучшившим Dependency Injection в Symfony 6.3, и особая благодарность Nicolas Grekas, который предоставил половину содержимого этой статьи, для объяснения самых продвинутых возможностей.

Дополнительные материалы

Предыдущая Статья

Laravel: Шлюз/Gate и Политика/Policy

Следующая Статья

Новое в Symfony 6.3 — Компонент AssetMapper