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.
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.
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);
}
}
A couple of services that implement FooInterface
class FooOneService extends AbstractFoo { }
class FooTwoService extends AbstractFoo { }
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();
}
}
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'
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With