-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Messenger] "Self-handling" messages (message AND handler in the same class) #58102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Just add my thoughts here. The handler can in your case not longer be a service which make things a lot harder. The reason is we can not construct a handler service in the dependency injection container as the A message in current case can have multiple handlers. Is that then still supported, self handled + additinal handlers? The self handled part is not a service which so would not allow service decoration via dependency injection component. So I think the first example where the PS: you may can achieve a simple prototype by creating an own HandlerLocator: https://github.com/symfony/messenger/blob/e1dc743492ff9f1cfb23e6eea3592bf2ec9bd079/Middleware/HandleMessageMiddleware.php#L36 or/and an own middleware setting the HandlerArgumentsStamp for additional dependencies: https://github.com/symfony/messenger/blob/e1dc743492ff9f1cfb23e6eea3592bf2ec9bd079/Middleware/HandleMessageMiddleware.php#L77 |
That's why the first example's
That's a great point, though. Maybe with the static method approach it could be registered in the DI somehow for handling (so the bus knows it has to call the static method), but it really messes up with decorating stuff or injecting handler into other services (actually we do it now, because we work on a migration to Messenger but in the first phase we just use old handlers that instantiate new command and call the new handler with it - the actual business logic is moved from old handler to the new one)... Did not think about this before. Thanks for pointing me to |
Yes and no 😅. The idea of the code being closer is one of the drivers here, the second one is naming convention. Having message and handler in the same file, but using separate classes does not solve the latter, as the handler still need to be named differently (mixing intent with technical jargon). This is not a big deal, as I said initially, I was just wondering if it's possible to achieve both under the same name (intent). @alexander-schranz's argument related to registering handler in the DI is crucial though, so unless someone comes up with some brilliant idea, I believe it's not doable 🤷. |
What might help you achieve it is the: But also think @kbond solved a similar issue with the dependencies in https://github.com/zenstruck/console-extra. But maybe we could rethink how we could move the Argument Resolver part for services from HttpKernel package to the Dependency Injection package, so we may not need duplicate things for controllers, commands and maybe message handlers in future. But think @kbond already have some thoughts on this part. |
For me both message and handler are application layer concepts (even though handler more or less execute domain logic) and from the intent point of view they both could be named the same. For example, when you want to add a product you have PS. This mostly applies to command and query buses, where you rather want to have single handler for a message, and then its name correlates with an intent. For event buses there's not a problem since the message is clear (something happened) and there may be multiple handlers not even related (name-wise) to event's name. Maybe naming convention could be flipped? |
Yes, for this package I use the I have often thought about extracting the logic that allows controllers to be autowired so that any callable can be used this way. I believe this to be the proper solution. |
I wouldn't encourage merging messages with handlers. The message captures and represents the intent, mixing it with the handling logic looks too much responsibilities and is less future-proof to me. |
@kbond Think it would be great to extract that part to the dependency-injection component. Maybe there something already exists which can be autopted as autowire over |
The dependency injection cannot perform runtime autowiring of a signature. The DI component is performing all autowiring by altering the service definitions during compilation of the template. the resolution of controller parameters is not handled by the DI component but by the ArgumentResolver of HttpKernel (there is indeed a ServiceValueResolver hooked into it that relies on some resolution done at compile time, but this is also implemented by HttpKernel, and its uses an extension point of the ArgumentResolver). |
@stof you're talking about how it's done now, but whether it's correct or not is IMHO a totally separate story. "Dependency Injection" as a concept is not strictly bound to container and cache, it's about... injecting dependencies 🤷. Injecting them at runtime is in my opinion a responsibility of dependency injection component more than HttpKernel component, as your runtime can be CLI which does not have HTTP context, but still may require injecting params/dependencies. Personally I believe this is one of Symfony's design flaws, and I hope it can be fixed somehow (just like profiling CLI commands, that wasn't possible for years 😅). |
I managed to create a naive user-space implementation on this 👇 It works by defining a non-shared service in DI which has a factory that fetches the message from namespace App\Message;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
final readonly class DoSomething
{
public function __construct(public string $person)
{
}
#[AsMessageHandler(handles: self::class)]
public function __invoke(LoggerInterface $logger): void
{
$logger->alert("Hello $this->person");
}
} namespace App\Messenger;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Handler\HandlerDescriptor;
use Symfony\Component\Messenger\Handler\HandlersLocatorInterface;
#[AsDecorator('messenger.bus.default.messenger.handlers_locator')]
class SelfHandlingLocator implements HandlersLocatorInterface
{
private ?object $message = null;
public function __construct(
private readonly HandlersLocatorInterface $inner,
#[AutowireLocator('messenger.self_handling_message')]
private readonly ContainerInterface $handlers,
) {
}
public function getCurrentMessage(): ?object
{
return ([$this->message] = [null, $this->message])[1];
}
public function getHandlers(Envelope $envelope): iterable
{
if ($this->handlers->has($envelope->getMessage()::class)) {
$this->message = $envelope->getMessage();
// Note: this probably does not support from_transport etc options
yield new HandlerDescriptor(fn () => $this->handlers->get($envelope->getMessage()::class));
return; // this implementation does not support having both, self-handler & normal handler on a single message class
}
yield from $this->inner->getHandlers($envelope);
}
} namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel implements CompilerPassInterface
{
use MicroKernelTrait;
public function process(ContainerBuilder $container): void
{
foreach ($container->findTaggedServiceIds('messenger.message_handler') as $serviceId => $tags) {
foreach ($tags as $attributes) {
$handles = $attributes['handles'] ?? null;
$definition = $container->getDefinition($serviceId);
$class = $definition->getClass();
if ($handles !== $class) {
continue;
}
$definition
->addMethodCall($attributes['method'] ?? '__invoke')
->addTag('messenger.self_handling_message')
->setShared(false)
->setFactory([new Reference('messenger.bus.default.messenger.handlers_locator'), 'getCurrentMessage']);
}
}
}
} |
Description
We had an interesting discussion about buses and my colleague shared how he implemented mechanism that mixes command and handler in the same class (which looks like an advantage for several people that attended into discussion). Imagine something like this:
Then with
$bus->execute(new DoSomething(new Foo()));
the bus callsexecute()
on aDoSomething
instance, additionally resolving all the dependencies and injecting them as a method's arguments.This approach is not applicable 1:1 to Messenger since currently the handlers receive the message instance as an argument rather than using object's context, but I was wondering if it's possible to achieve self-executing commands in Messenger 🤔? There is
HandlerArgumentsStamp
that could be helpful here probably (but rather cumbersome), but my actual thought is: would it be possible to support#[Autowire]
in a method marked with#[AsCommandHandler]
, so the dependencies are resolved dynamically, when the message is handled?FYI: I don't have a problem with current approach, where command and handler are in separate classes. I put them into separate directories, so I can import whole handlers' dir into DI, but the naming convention is somehow bothering me: why do I actually need
DoSomething
andDoSomethingHandler
? I like naming that represents the intent, not technical side of it, so for me more natural would be to useDoSomething
for handler too. But then, with message and command in separate files it's required to keep them in separate namespace and alias the command in the handler anyway.Please correct me if I'm wrong and it's already possible to achieve this 🙂. I tried to find it in the official docs, but it seems like it's not possible currently. As far as I know in Laravel it's possible and I personally think it's totally OK (side note: I dislike messages to be self-dispatchable though).
It does not make any difference between injecting dependencies through constructor and using them in
__handle()
, and injecting them through a method marked with#[AsMessageHandler]
- in both cases dependencies come from the outside, so the handler remains flexible and testable.Example
Using message instance as an explicit argument in handler (just like it's done now):
Using message instance's context (handler method receives only required dependencies):
The text was updated successfully, but these errors were encountered: