Skip to content
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

[Messenger] Add Envelope::getResult() #49294

Open
wants to merge 1 commit into
base: 6.3
Choose a base branch
from

Conversation

lyrixx
Copy link
Member

@lyrixx lyrixx commented Feb 8, 2023

Q A
Branch? 6.3
Bug fix? no
New feature? yes
Deprecations?
Tickets
License MIT
Doc PR

we were reading https://thomas.jarrand.fr/blog/cache-query-avec-symfony-messenger/ and a line triggered @damienalexandre:

$envelope->last(HandledStamp::class)->getResult()

I agree this line is not very optimal:

  • it's long
  • you need to know internals to write it

That's why I propose a shortcut


If this feature is accepted, I'll write

  • A test
  • CHANGELOG.md

@carsonbot carsonbot added this to the 6.3 milestone Feb 8, 2023
@OskarStark OskarStark changed the title [Messenger] Add Envelope::getResult() [Messenger] Add Envelope::getResult() Feb 8, 2023
@dreadnip
Copy link
Contributor

dreadnip commented Feb 9, 2023

We already have https://github.com/symfony/symfony/blob/6.2/src/Symfony/Component/Messenger/HandleTrait.php which kind of achieves the same goal as your proposed method, but it doesn't play nice with invokable controllers so I personally avoid using it.

In the docs: https://symfony.com/doc/current/messenger/handler_results.html#working-with-command-query-buses, it is used like this:

class ListItems
{
    use HandleTrait;

    public function __construct(MessageBusInterface $messageBus)
    {
        $this->messageBus = $messageBus;
    }

    public function __invoke()
    {
        $result = $this->handle(new ListItemsQuery(/* ... */));

        // Do something with the result
        // ...
    }
}

but most controllers these days look like this:

class ListItemsController
{
    use HandleTrait;

    #[Route(path: '/', name: 'demo']
    public function __invoke(
        MessageBusInterface $messageBus
    ) {
        $result = $this->handle(new ListItemsQuery(/* ... */));

        // Do something with the result
        // ...
    }
}

which doesn't work with the trait. I'd have to add an explicit constructor, which I don't want to do.

I like moving the code from the trait into the Response::getResult() method you've suggested for this reason.

@chalasr
Copy link
Member

chalasr commented Feb 9, 2023

We'd have to deprecate HandleTrait then. Not sure if it's worth it yet, personally.

@brzuchal
Copy link
Contributor

brzuchal commented Feb 9, 2023

class QueryGateway
{
    use HandleTrait;

    public function __construct(
        #[Autowire('query_bus')] private MessageBusInterface $messageBus,
    ) {
    }
}
class ListItemsController
{
    #[Route(path: '/', name: 'demo']
    public function __invoke(QueryGateway $queryGateway)
    {
        $result = $queryGateway->handle(new ListItemsQuery(/* ... */));
        // Do something with the result
        // ...
    }
}

Where's the problem?

EDIT:
If the handle() method looks somewhat inappropriate every trait in PHP allows renaming methods, for eg:

class QueryGateway
{
    use HandleTrait { handle as public query; }

    public function __construct(
        #[Autowire('query_bus')] private MessageBusInterface $messageBus,
    ) {
    }
}

So instead of calling $queryGateway->handle(new ListItemsQuery(/* ... */)); it is possible to call $queryGateway->query(new ListItemsQuery(/* ... */));.

@dreadnip
Copy link
Contributor

dreadnip commented Feb 9, 2023

Smart!

But if the trait requires a separate "gateway" class with nothing but a constructor to be useful, doesn't this mean that the design/DX of the current solution could be improved?

@brzuchal
Copy link
Contributor

brzuchal commented Feb 9, 2023

@dreadnip like everything - it depends.
From my perspective, this increases the DX significantly.
Now I can type-hint my method with a QueryGateway so the developer understands what the desire for it is also I no longer have to care about figuring out which one is the good bus and what is the right name for it because I did the wiring in one place and now have a small class with one responsibility given.

@brzuchal
Copy link
Contributor

brzuchal commented Feb 9, 2023

Personally, I use this pattern as it's easier to type-hint ctor of my handlers or controllers like in this example with a *Gateway which additionally has one simple responsibility.

The idea can be extended for eg. with the expected return type and additional assertion on *Gateway side with a help of a small middleware in a bus that runs a lookup on all detected handlers and chooses the one giving the expected result type - this way you can also improve your DX with static analysis like Psalm or PHPStan cus the handle/query method on a *Gateday can be annotated with a @template param.

You could compare it to a serializer mechanism using the right normalizer depending on your expected deserialization type where the expected return type is given upfront and the magic inside is responsible to ensure you can get it if possible.

AFAIK the Messenger component was designed with simplicity in mind so in general the use cases we discuss here IMHO should be a subject of a standalone component if ever.

If you start thinking about separate buses for queries and commands and events you see that one simple MessageBusInterface with only a default service behind it is too simple for that purpose and not enough.
That is because the nature of for eg. EventGateway is to dispatch an event but don't expect it to be handled at all. Another is CommandGateway which should handle but the handling could be done in async. While QueryGateway purpose is to run in sync and always handle directly and strictly by one handler.

@ro0NL
Copy link
Contributor

ro0NL commented Feb 9, 2023

but most controllers these days look like this:

im not sure most controllers these days use services as controller arguments

@brzuchal
Copy link
Contributor

brzuchal commented Feb 9, 2023

I agree with @ro0NL , for eg. most if not all controllers look as follows:

#[AsController,Route(path: '/', name: 'demo']
class ListItemsController
{
    public function __construct(
        private QueryGateway $queryGateway
    ) {
    }

    public function __invoke(): Response
    {
        $result = $this->queryGateway->query(new ListItemsQuery(/* ... */));
        // Do something with the result
        // ...
    }
}

@jorismak
Copy link

jorismak commented Feb 9, 2023

I agree with @ro0NL , for eg. most if not all controllers look as follows:

#[AsController,Route(path: '/', name: 'demo']
class ListItemsController
{
    public function __construct(
        private QueryGateway $queryGateway
    ) {
    }

    public function __invoke(): Response
    {
        $result = $this->queryGateway->query(new ListItemsQuery(/* ... */));
        // Do something with the result
        // ...
    }
}

I think we must find a solution to work with both , otherwise this ends up being a yes! No! Yes! No! discussion.

I haven't used an invoke constructor for years for instance, and haven't seen one on symfony casts in a while. Not to say one is better.... But to prove both are valid and must be supported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants