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

Support for Messenger HandleTrait return types #404

Open
wants to merge 13 commits into
base: 1.4.x
Choose a base branch
from

Conversation

bnowak
Copy link

@bnowak bnowak commented Aug 13, 2024

The goal is to cover #207.

That's my very first PR in phpstan-symfony plugin, so any feedback/tips/advices are very welcome :)

@bnowak bnowak force-pushed the messenger-handle-trait-support branch from 5d47f63 to e3790e0 Compare August 14, 2024 05:53
Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of DynamicMethodReturnTypeExtension you can use https://apiref.phpstan.org/1.11.x/PHPStan.Type.ExpressionTypeResolverExtension.html which is like that but lets you override any expression type.

@bnowak bnowak force-pushed the messenger-handle-trait-support branch from e7063fd to 7b4c17e Compare October 15, 2024 06:23
@bnowak bnowak marked this pull request as ready for review October 17, 2024 14:31
@bnowak
Copy link
Author

bnowak commented Oct 17, 2024

@ondrejmirtes Thanks for the tip about ExpressionTypeResolverExtension! It resolved my blocker issue about not supported traits in DynamicMethodReturnTypeExtension.

It took me some time, however finally I've got ready to review version of this PR :) Please take a look in you free time :)

Thank you very much!

@bnowak
Copy link
Author

bnowak commented Oct 17, 2024

As static analyzing should works now fine with the result of HandleTrait::handle, but only in classes which have statically typed query passed - like in this SF-adjusted example:

// src/Action/ListItems.php
namespace App\Action;

use App\Message\ListItemsQuery;
use App\MessageHandler\ListItemsQueryResult;
use Symfony\Component\Messenger\HandleTrait;
use Symfony\Component\Messenger\MessageBusInterface;

class ListItems
{
    use HandleTrait;

    public function __construct(
        private MessageBusInterface $messageBus,
    ) {
    }

    public function __invoke(): void
    {
        $result = $this->handle(new ListItemsQuery(/* ... */));
        // $result should be automatically recognized as `ListItemsQueryResult` now by phpstan via SF configuration

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

However, my target goal was to cover QueryBus classes with dynamic return types based on dynamic query, something like:

// src/MessageBus/QueryBus.php
namespace App\MessageBus;

use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\HandleTrait;
use Symfony\Component\Messenger\MessageBusInterface;

class QueryBus
{
    use HandleTrait;

    public function __construct(
        private MessageBusInterface $messageBus,
    ) {
    }

    // wherever that method is called, phpstan should recognize return type based on passed query
    public function query($query)
    {
        return $this->handle($query);
    }
}

Maybe it's trivial and doable with already existing php-doc annotation or more complex to write some next extension for that, but I don't know how to connect the dots and reuse covered typing of HandleTrait::handle which is already in place. I'm happy to address that in separate PR (if will be needed to achieve that) :)
Any thoughts/ideas are more than welcome :)
/cc @ondrejmirtes

src/Symfony/MessageMap.php Show resolved Hide resolved
src/Symfony/MessageMap.php Outdated Show resolved Hide resolved
src/Symfony/MessageMapFactory.php Outdated Show resolved Hide resolved
src/Symfony/MessageMapFactory.php Show resolved Hide resolved
@bnowak bnowak force-pushed the messenger-handle-trait-support branch from b94aa51 to d62ec03 Compare November 7, 2024 12:30
@bnowak
Copy link
Author

bnowak commented Nov 20, 2024

@ondrejmirtes any chances to review that, please? 🙏

@bnowak
Copy link
Author

bnowak commented Dec 18, 2024

@ondrejmirtes I can see that version 2.0.0 is released now. Is 1.x abandoned from now? Should I rebase that PR to 2.x branch? Any chances to review and/or merge it please? 🙂

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to rebase this, we could release this in 1.x.

.gitignore Outdated
@@ -1,5 +1,6 @@
/tests/tmp
/build-cs
/vendor
/.idea
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this please. This belongs to a global .gitignore.


/** @var array{handles?: class-string, method?: string} $tagAttributes */
$tagAttributes = $tag->getAttributes();
$reflectionClass = $this->reflectionProvider->getClass($serviceClass);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unsafe. Ask for hasClass first.

}

foreach ($handles as $messageClassName => $options) {
$methodReflection = $reflectionClass->getNativeMethod($options['method'] ?? self::DEFAULT_HANDLER_METHOD);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unsafe. Ask for hasNativeMethod first.

private function guessHandledMessages(ClassReflection $reflectionClass): iterable
{
if ($reflectionClass->implementsInterface(MessageSubscriberInterface::class)) {
foreach ($reflectionClass->getName()::getHandledMessages() as $index => $value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Save the class name into a variable first, then call the method.

return new MessageMap($messageMap);
}

/** @return array<class-string, array<string, string>> */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not return an array. It's a generator.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, changed to iterable :)

}

/** @var array{handles?: class-string, method?: string} $tagAttributes */
$tagAttributes = $tag->getAttributes();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a @return stub for this method instead of @var?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shape is dynamic and rely on what tag we're using from SF config. In this case I'm ensuring above that we're handling only messenger.message_handler, so we know what shape it should have.

As far I know stubs are static only, so unfortunately we cannot use them here.

Do you have other idea or it could stay as it is?

* @phpstan-assert-if-true class-string $index
* @phpstan-assert-if-true array<string, mixed> $value
* @phpstan-assert-if-false int $index
* @phpstan-assert-if-false class-string $value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ($this->isSupported($expr, $scope)) {
$arg = $expr->getArgs()[0]->value;
/** @var class-string[] $argClassNames */
$argClassNames = $scope->getType($arg)->getObjectClassNames();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't add this @var.

}

/**
* @phpstan-assert-if-true MethodCall $expr
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs =.


return $declaringClassReflection->getName() === self::TRAIT_NAME;
} catch (ReflectionException $e) {
return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of catching exception, check if the method exists first.

@bnowak
Copy link
Author

bnowak commented Dec 23, 2024

@ondrejmirtes Thanks for review. I addressed all your comments, please take a look again when you have time :)

Also, could you take a look on #404 (comment) and answer that if you have an idea how to solve it please? That's my initial goal to resolve by this PR :)

} else {
yield $value => ['method' => self::DEFAULT_HANDLER_METHOD];
}
} catch (ShouldNotHappenException $e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never catch ShouldNotHappenException.

public function getType(Expr $expr, Scope $scope): ?Type
{
if ($this->isSupported($expr, $scope)) {
$arg = $expr->getArgs()[0]->value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$expr->getArgs()[0] might not exist. Check the existence first.

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

Successfully merging this pull request may close these issues.

3 participants