New in Symfony 4.3: Indexed and Tagged Service Collections

Symfony provides a shortcut to inject all services tagged with a specific tag, which is a common need in some applications, so you don't have to write a compiler pass just for that. In Symfony 4.3 we improved this to allow accessing the tagged services by your own defined index.

In the following example, services tagged with app.handler define an additional attribute called key. When injecting them into the App\HandlerCollection service, you can now define an attribute called index_by to tell Symfony which is the index that should be used in the associative array that contains the tagged services:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# config/services.yaml
services:
    App\Handler\One:
        tags:
            - { name: 'app.handler', key: 'handler_one' }
    App\Handler\Two:
        tags:
            - { name: 'app.handler', key: 'handler_two' }

    App\HandlerCollection:
        # inject all services tagged with app.handler as first argument
        # and use the value of the 'key' tag attribute to index the services
        arguments: [!tagged { tag: 'app.handler', index_by: 'key' }]

After compiling the service container, the HandlerCollection service can iterate over the handlers using the values defined in their key attributes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/Handler/HandlerCollection.php
namespace App\Handler;

class HandlerCollection
{
    public function __construct(iterable $handlers)
    {
        $handlers = iterator_to_array($handlers);
        $handlerTwo = $handlers['handler_two'];
        // ...
    }
}

Instead of defining the index value in each service tag, you can define this value in a static method called getDefaultIndexName() in your service. For example, this is how the previous App\Handler\One service would look now:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/Handler/One.php
namespace App\Handler;

class One
{
    // ...

    public static function getDefaultIndexName(): string
    {
        return 'handler_one';
    }
}

The name of this static method is also configurable via the default_index_method tag attribute:

1
2
3
4
5
6
# config/services.yaml
services:
    # ...

    App\HandlerCollection:
        arguments: [!tagged { tag: 'app.handler', default_index_method: 'someCustomMethodName' }]

Comments

Unfortunately this is quite useless. Collections of services like these need to be lazy because most of the time you only need a few of them and initializing the rest is useless overhead. Instead of an iterable the HandlerCollection should receive a Closure that would return the service associated with given key.
@Jáchym for cases where you need to access by key (rather than using the key during iteration), what you likely want is to use the service_locator system (it does not inject a Closure, but a PSR-11 ContainerInterface implementation with get and has methods). But the great news is that this new feature will soon be usable inside the service_location configuration to configure things easily: https://github.com/symfony/symfony/pull/30348
Shouldn't the One class implement some kind of interface, so we know that the getDefaultIndexName() method is what we expect and not just an unexpected name-alike method?
Josef you are right this should be done but isnt a hard requirement
Login with SymfonyConnect to post a comment