Is there a way to inject all services that have a specific tag / implement a specific interface?

jahudka
Member | 66
+
0
-

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 IMenuItemProviders 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)

enumag
Member | 2128
+
+1
-

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:

  1. 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
  1. 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)

Filip Procházka
Moderator | 4693
+
0
-

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.