symfonyconfigurationbundleservice-locator

Symfony 6 bundle: services tagged via ServiceConfigurator::load() are not registered (custom tag not found)


I’m developing a custom Symfony bundle that provides a state machine system. I am using Symfony 6.2.7.

The bundle user must configure it like this:

# config/packages/state_machine.yaml
state_machine:
  definition:
    directory: '%kernel.project_dir%/smdef'
  commands:
    namespace: 'App\StateMachine\Commands\'
    directory: '%kernel.project_dir%/src/StateMachine/Commands'

In my bundle extension, I try to automatically register all command classes located in this directory and tag them:

final class StateMachineBundle extends AbstractBundle {
...
    public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void {
        $services = $container->services();
        $services->defaults()->autowire()->autoconfigure()->private();
        $cmdDir = $builder->getParameterBag()->resolveValue($config['commands']['directory']);

        if (!is_dir($cmdDir)) {
            throw new \RuntimeException(sprintf('Directory not found: %s', $cmdDir));
        }

        $services
            ->load($config['commands']['namespace'], "$cmdDir/*")
                ->tag('state_machine.command');
        ...
    }
...
}

The goal is to have one service per command class (all implementing CommandInterface) so they can later be fetched dynamically by FQCN.

I inject them using a tagged locator:

// services.php
...
$services
    ->set('state_machine.command_executor', CommandExecutor::class)
    ->args([
        tagged_locator('state_machine.command', 'class'),
    ])
    ->alias(CommandExecutor::class, 'state_machine.command_executor');
...

The executor looks like this:

final class CommandExecutor implements CommandExecutorInterface
{
    public function __construct(private readonly ContainerInterface $commandLocator) {}

    public function execute(\Iterator $listOfCommands, Stateful $stateful): ResultInterface
    {
        try {
            foreach ($listOfCommands as $commandFqcn) {
                if (!$this->commandLocator->has($commandFqcn)) {
                    throw new CommandNotRegisteredException($commandFqcn);
                }

                $command = $this->commandLocator->get($commandFqcn);
                $command->execute($stateful);
            }
        } ...
    }
}

My problem: CommandNotRegisteredException is always thrown.

This is confirmed when I run:

bin/console debug:container --tag=state_machine.command

I get:

No tags found that match "state_machine.command"

Notes:

What could prevent services from being registered/tagged in this case? Is the load() resource pattern incorrect, or am I missing something about how Symfony resolves/keeps tagged services?


Solution

  • I think you're making this too complex. You shouldn't need the commands namespace/directory config at all. As long as users have the directory where their commands are located autowired/autoconfigured (this would be by default in a standard Symfony app). You can change loadExtension() to:

    public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
    {
        $builder
            ->registerForAutoconfiguration(CommandInterface::class)
                ->addTag('state_machine.command')
        ;
    }
    

    See this doc for more information.