Get service via class name from iterable – injected tagged services

You no longer (since Symfony 4) need to create a compiler pass to configure a service locator.

It’s possible to do everything through configuration and let Symfony perform the “magic”.

You can make do with the following additions to your configuration:

services:
  _instanceof:
    DriverInterface:
      tags: ['app.driver']
      lazy: true

  DriverConsumer:
    arguments:
      - !tagged_locator
        tag: 'app.driver'

The service that needs to access these instead of receiving an iterable, receives the ServiceLocatorInterface:

class DriverConsumer
{
    private $drivers;
    
    public function __construct(ServiceLocatorInterface $locator) 
    {
        $this->locator = $locator;
    }
    
    public function foo() {
        $driver = $this->locator->get(Driver::class);
        // where Driver is a concrete implementation of DriverInterface
    }
}

And that’s it. You do not need anything else, it just workstm.


Complete example

A full example with all the classes involved.

We have:

FooInterface:

interface FooInterface
{
    public function whoAmI(): string;
}

AbstractFoo

To ease implementation, an abstract class which we’ll extend in our concrete services:

abstract class AbstractFoo implements FooInterface
{
    public function whoAmI(): string {
        return get_class($this);
    }   
}

Services implementations

A couple of services that implement FooInterface

class FooOneService extends AbstractFoo { }
class FooTwoService extends AbstractFoo { }

Services’ consumer

And another service that requires a service locator to use these two we just defined:

class Bar
{
    /**
     * @var \Symfony\Component\DependencyInjection\ServiceLocator
     */
    private $service_locator;

    public function __construct(ServiceLocator $service_locator) {
        $this->service_locator = $service_locator;
    }

    public function handle(): string {
        /** @var \App\Test\FooInterface $service */
        $service = $this->service_locator->get(FooOneService::class);

        return $service->whoAmI();
    }
}

Configuration

The only configuration needed would be this:

services:
  _instanceof:
    App\Test\FooInterface:
      tags: ['test_foo_tag']
      lazy: true
    
  App\Test\Bar:
      arguments:
        - !tagged_locator
          tag: 'test_foo_tag'
            

Alternative to FQCN for service names

If instead of using the class name you want to define your own service names, you can use a static method to define the service name. The configuration would change to:

App\Test\Bar:
        arguments:
          - !tagged_locator
            tag: 'test_foo_tag'
            default_index_method: 'fooIndex'

where fooIndex is a public static method defined on each of the services that returns a string. Caution: if you use this method, you won’t be able to get the services by their class names.

Leave a Comment