Как добавить тег на объекты определенного типа в Symfony DI?

Ответ

В Symfony Dependency Injection есть несколько способов автоматически назначать теги сервисам.

1. Использование _instanceof в конфигурации services.yaml: Это основной метод для автоматической разметки всех сервисов, реализующих определенный интерфейс или наследующих класс.

services:
    _instanceof:
        AppServiceMessageHandlerInterface:
            tags: ['app.message_handler']

Все сервисы, реализующие MessageHandlerInterface, получат тег app.message_handler.

2. Использование атрибутов (начиная с Symfony 6.1): Можно помечать классы сервисов атрибутом #[AutoconfigureTag].

use SymfonyComponentDependencyInjectionAttributeAutoconfigureTag;

#[AutoconfigureTag('app.message_handler')]
class EmailHandler implements MessageHandlerInterface
{
    // ...
}

3. Создание Compiler Pass для сложной логики: Если нужна динамическая логика назначения тегов, создается кастомный CompilerPass.

use SymfonyComponentDependencyInjectionCompilerCompilerPassInterface;
use SymfonyComponentDependencyInjectionContainerBuilder;

class CustomTagCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        foreach ($container->findDefinitions() as $id => $definition) {
            $class = $definition->getClass();
            if ($class && is_subclass_of($class, SomeBaseClass::class)) {
                $definition->addTag('custom.tag');
            }
        }
    }
}

Этот CompilerPass нужно зарегистрировать в методе build() вашего Kernel.

4. Получение тегированных сервисов: В коде или в другом CompilerPass можно получить все сервисы с определенным тегом.

// В CompilerPass
$taggedServices = $container->findTaggedServiceIds('app.message_handler');

// В контроллере или сервисе через инжектированный ContainerBuilder
$handlers = $this->container->get('service_container')->findTaggedServiceIds('app.message_handler');

Почему это важно: Автоматическое тегирование избавляет от ручной регистрации каждого сервиса в конфигурации, делает систему более масштабируемой и соответствует принципу Convention Over Configuration.

Ответ 18+ 🔞

Ёпта, ну слушай, тут про автоматическое тегирование в Symfony Dependency Injection. Просто овердохуища способов, чтобы не париться и не прописывать каждый сервис руками, как будто на дворе 2002-й год.

1. Самый прикол — _instanceof в services.yaml: Это, бля, базовая фишка, чтобы автоматом навешивать теги на все сервисы, которые реализуют какой-то интерфейс или класс.

services:
    _instanceof:
        AppServiceMessageHandlerInterface:
            tags: ['app.message_handler']

Вот и всё, чувак. Любой сервис, который заявляет, что он MessageHandlerInterface, сразу получает тег app.message_handler. Никакой возни, мать его.

2. Атрибуты, с версии 6.1: Тут вообще красота — можно класс пометить атрибутом, и он сам себя протегирует. Выглядит как хитрая жопа, но работает.

use SymfonyComponentDependencyInjectionAttributeAutoconfigureTag;

#[AutoconfigureTag('app.message_handler')]
class EmailHandler implements MessageHandlerInterface
{
    // ...
}

Написал и забыл, блядь. Компилятор сам всё подхватит.

3. Кастомный Compiler Pass для особо извращённых случаев: Если логика тегирования сложная, на уровне "а если у него есть метод handle() и имя класса заканчивается на 'Listener'", то тут без своего Compiler Pass'а — никуда.

use SymfonyComponentDependencyInjectionCompilerCompilerPassInterface;
use SymfonyComponentDependencyInjectionContainerBuilder;

class CustomTagCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        foreach ($container->findDefinitions() as $id => $definition) {
            $class = $definition->getClass();
            if ($class && is_subclass_of($class, SomeBaseClass::class)) {
                $definition->addTag('custom.tag');
            }
        }
    }
}

Этот пасс, понятное дело, нужно ещё в ядре зарегистрировать в методе build(). Но это уже детали, ебать.

4. Как потом эти тегированные сервисы получить: Ну а дальше-то что? А дальше их нужно где-то использовать.

// Внутри другого CompilerPass'а
$taggedServices = $container->findTaggedServiceIds('app.message_handler');

// Или прямо в коде, если контейнер под рукой
$handlers = $this->container->get('service_container')->findTaggedServiceIds('app.message_handler');

А зачем это всё, спрашивается? А затем, чувак, чтобы не быть распиздяем. Вместо того чтобы в конфиге руками прописывать сотню хендлеров, ты один раз написал правило — и система сама всё расставила. Масштабируется на раз-два, соответствует принципу "меньше конфигурации — больше договорённостей". В общем, ебушки-воробушки, удобная штука.