Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get service via class name from iterable - injected tagged services

I am struggling to get a specific service via class name from group of injected tagged services.

Here is an example: I tag all the services that implement DriverInterface as app.driver and bind it to the $drivers variable.

In some other service I need to get all those drivers that are tagged app.driver and instantiate and use only few of them. But what drivers will be needed is dynamic.

services.yml

_defaults:
        autowire: true
        autoconfigure: true
        public: false
        bind:
            $drivers: [!tagged app.driver]

_instanceof:
        DriverInterface:
            tags: ['app.driver']

Some other service:

/**
 * @var iterable
 */
private $drivers;

/**
 * @param iterable $drivers
 */
public function __construct(iterable $drivers) 
{
    $this->drivers = $drivers;
}

public function getDriverByClassName(string $className): DriverInterface
{
    ????????
}

So services that implements DriverInterface are injected to $this->drivers param as iterable result. I can only foreach through them, but then all services will be instantiated.

Is there some other way to inject those services to get a specific service via class name from them without instantiating others?

I know there is a possibility to make those drivers public and use container instead, but I would like to avoid injecting container into services if it's possible to do it some other way.

like image 459
povs Avatar asked Mar 01 '19 14:03

povs


1 Answers

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.

like image 83
yivi Avatar answered Sep 23 '22 10:09

yivi