Kevin Bond Nicolas Grekas
Contributed by Kevin Bond and Nicolas Grekas in #51392 and #51832

Sometimes, services need access to several other services without being sure that all of them will actually be used. Injecting all services can hurt performance (because Symfony will instantiate all of them, even unused ones) and injecting the entire container is strongly discouraged in Symfony applications.

The best solution in those cases is to use service subscribers and locators. A service locator is like a custom service container that only includes the services that you selected.

In Symfony 6.4 we're improving service locators so you can also define them using PHP attributes instead of configuration files. The new #[AutowireLocator] attribute takes a single service ID or an array of service IDs as its first argument:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use App\CommandHandler\BarHandler;
use App\CommandHandler\FooHandler;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;

class SomeService
{
    public function __construct(
        #[AutowireLocator([FooHandler::class, BarHandler::class])]
        private ContainerInterface $handlers,
    ) {
    }

    public function someMethod(): void
    {
        $fooService = $this->handlers->get(FooHandler::class);
    }
}

You can also define aliases for these services and even include optional services by prefixing the service class with a ? symbol:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use App\CommandHandler\BarHandler;
use App\CommandHandler\FooHandler;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Symfony\Contracts\Service\Attribute\SubscribedService;

class SomeService
{
    public function __construct(
        #[AutowireLocator([
            'foo' => FooHandler::class,
            'bar' => new SubscribedService(type: 'string', attributes: new Autowire('%some.parameter%')),
            'optionalBaz' => '?'.BazHandler::class,
        ])]
        private ContainerInterface $handlers,
    ) {
    }

    public function someMethod(): void
    {
        $fooService = $this->handlers->get('foo');

        if ($this->handlers->has('optionalBaz')) {
            // ...
        }
    }
}

Check out the #[AutowireLocator] source to learn about its other arguments, such as $indexAttribute, $defaultPriorityMethod, $exclude, etc.

If you prefer to receive an iterable instead of a service locator, replace the AutowireLocator attribute by AutowireIterator.

Published in #Living on the edge