Is there a way to inject all services that have a specific tag / implement a specific interface?
- jahudka
- Member | 71
I have a service in my app that provides menu items to the admin section, so
that the user can pick from existing items when building a menu instead of
having to type the labels and URLs by hand. Other services throughout the app
implement an interface called IMenuItemProvider
, which has a method
called getAvailableMenuItems();
this method returns URL / label
pairs which I can then use to populate an add-menu-item form.
What I'm looking for is a way to inject all the available
IMenuItemProvider
s in the container to the menu service so that
I don't have to register them manually (the system is supposed to be modular).
So what I'd like to be able to do is something like:
services:
menu:
class: Some\Menu\Service\Class()
setup:
- addItemProviders(@container::findByType('IMenuItemProvider'))
The problem is obviously that the container's findByType()
method returns only the names of the matching services along with their
metadata. I know that the menu service could be made container-aware and so
able to pull the required services out of the container, but I wouldn't like to
go down that road because I think that using container-aware services is a DI
anti-pattern. Or I could create a DI extension for the menu which would add
addItemProvider()
calls to the menu service's definition on
compile time for every service implementing the IMenuItemProvider
interface; that's also a thing I'd like to avoid because it's overkill.
So the question is: short of making the menu service container-aware and building a dedicated DI extension, is there any way to inject all services of a given type (or, for that matter, all services having a given tag) to a setup method of a service?
Last edited by jahudka (2015-07-28 22:02)
- Filip Procházka
- Moderator | 4668
There is currently no way to inject all services of given type. There is
findByType
that returns only names, so you would have to iterate
over them and you need container for that. You could hack it with code-setup,
but that's not a very nice solution.
I usualy create such service ContainerAware, there is really no reason not to. As long as you don't do it with every service, but only with factory-type-services it's fine. Also, it's more effective than writing CompilerExtension that would generate the setup, because such service would initialize all the other services every time it's created.
It really depends on what behaviour you're trying to achieve.
- enumag
- Member | 2118
I have an extension to do just what you need because I already needed it like 5 times. Basically for each tag / interface I create another service called “Resolver” that returns wanted services as needed. Then I pass this service to wherever I need all the implementations of that interface. There are two use-cases:
- Let's say you're implementing sth like SF/Validator. For each constraint type you need a specific class that validates it. In case of SF/Validator those don't have to be services but let's say you need that. What you want is something like an array where index would be the constraint name and value the implementation you need. For performance reason you don't want to pass an array of services but want it to be lazy so you pass a callback or object (resolver) instead. Here is a simplified code of what I have in mind and how I do it.
class Validator {
private $resolver;
public function __construct(Resolver $resolver)
{
$this->resolver = $resolver; // resolver for ConstraintValidatorInterface
}
public funciton validate($constraint, $value)
{
return $this->resolver->resolve(get_class($constraint))->validate($value);
}
}
extensions:
resolvers: ResolversExtension
resolvers:
validators: ConstraintValidatorInterface
services:
validator:
class: Validator
arguments: [ @resolvers.validators ]
someConstraintValidator:
class: SomeConstraintValidator
tags:
resolver.name: SomeConstraintValidator
# pass 'SomeConstraintValidator' to @resolvers.validators->resolve() to get this service
- Second use case is that you do indeed need all of those services, in which case you need the array. For me the Resolver implements Traversable so I can iterate over all the services in question.
I'm considering to make this extension public but I'm not too sure about some aspects. First while the use cases I mentioned are not mutually exclussive in theory, they ususally are in practice. Should I separate the implementation (make Resolver not implement Traversable)? Second should the extension support something like a “fallback” for each resolver? Third I needed a resolver that would return one service for doctrine entities and work normally for others. List of entities is not available compile-time though so I had to hack it (override the resolver manually). I have a mechanism to do that built in but I'm not completely sure about it.
Last edited by enumag (2015-07-29 08:09)