1
votes

I got a class which accepts multiple Consumer implementations as constructor arguments.

I want to "fill in" all my Consumers via the Symfony DI-Container. I tried injection tagged services.

final class SynchronousMessageDispatcher implements MessageDispatcher
{
    /**
     * @var Consumer[]
     */
    private $consumers;

    public function __construct(Consumer ...$consumers)
    {
        $this->consumers = $consumers;
    }
}

So I tried to Tag the services in the services.yml like that:

services:
    _instanceof:
        EventSauce\EventSourcing\Consumer:
            tags: ['eventsauce.consumer']

And then inject it like this:

eventsauce.message_dispatcher:
    class: EventSauce\EventSourcing\SynchronousMessageDispatcher
    arguments: [!tagged eventsauce.consumer]

Now I'm getting the following error:

Argument 1 passed to EventSauce\EventSourcing\SynchronousMessageDispatcher::__construct() must implement interface EventSauce\EventSourcing\Consumer, instance of Symfony\Component\DependencyInjection\Argument\RewindableGenerator given

I fully understand why. Is there a way to unpack services

In other words: Is it possible to modify [!tagged eventsauce.consumer] somehow. Or is the ...$consumers syntax incompatible with the Tagged service Injection in Symfony.

Don't get me wrong. I know that I can easily implement MessageDispatcher myself. Just wanted to know ;-)

2
You'd have to rewrite custom !tagged functionality, see this line: github.com/symfony/symfony/pull/22200/… - Tomas Votruba

2 Answers

1
votes

My original solution:

As "Tomáš Votruba" mentioned you'd have to rewrite your own !tagged functionality. e.g. !tagged-variadic.

This is not worth the effort for me. I'd rather implement the class using an iteratable ("nifr" explained the benefits, thanks).

For further reading, there is a closed issue on symfony/symfony#23608

My new solution

I used Argument unpacking and the Delegation pattern to use the class the library provided with my tagged services.

Work :-) Hurray.

final class TaggedMessageDispatcher implements MessageDispatcher {
    public function __construct(iterable $consumers)
    {
        $this->dispatcher = new SynchronousMessageDispatcher(... $consumers);
    }

    public function dispatch(Message ...$messages): void
    {
        $this->dispatcher->dispatch(... $messages);
    }
}
0
votes

You're using a wrong typehint here.

With the [!tagged <tag>] syntax a single iterable will be injected - not an undefined number of arguments as expected by the splat operator.

You're actually typehinting for multiple Consumer objects as arguments with the splat (...$arguments) operator here.

So the answer to your question is: The splat operator is not compatible with the [!tagged ..] syntax. You'd indeed need to write your own injection type that splits up the tagged services when using a new notation like [!tagged-call_user_func ..].

That said it doesn't really make sense to collect a list of objects, extract them to be function arguments just to let PHP put them back into a list again. I get your idea behind it in terms of code cleanliness though.

Another limitation is the fact that you can't pass multiple variadic arguments to a function. So ...

public function __construct(Alpha ...$alphas, Beta ...$betas)

... is not possible.

A possible solution/workaround allowing you to keep the typehinting for the collection would be the following:

final class SynchronousMessageDispatcher implements MessageDispatcher
{
    /**
     * @var Consumer[]
     */
    private $consumers;

    public function __construct(iterable $consumers)
    {
        foreach($consumers as $consumer) {
          assert($consumer instanceof Consumer, \InvalidArgumentException('..'));
        }

        $this->consumers = $consumers;
    }
}