-
Notifications
You must be signed in to change notification settings - Fork 93
Support for Messenger HandleTrait return types #404
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
Changes from 12 commits
4c95d32
5985362
bd75cb6
9768de5
0c8cb34
224c478
9438e5e
8a44be8
46e0cc8
5cba4ba
f7bf1e0
d62ec03
89c7534
df90a3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
/tests/tmp | ||
/build-cs | ||
/vendor | ||
/.idea | ||
/composer.lock | ||
.phpunit.result.cache |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Symfony; | ||
|
||
use PHPStan\Type\Type; | ||
|
||
final class MessageMap | ||
{ | ||
|
||
/** @var array<class-string, Type> */ | ||
private $messageMap; | ||
|
||
/** @param array<class-string, Type> $messageMap */ | ||
public function __construct(array $messageMap) | ||
{ | ||
$this->messageMap = $messageMap; | ||
} | ||
|
||
/** @param class-string $class */ | ||
public function getTypeForClass(string $class): ?Type | ||
{ | ||
return $this->messageMap[$class] ?? null; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Symfony; | ||
|
||
use PHPStan\Reflection\ClassReflection; | ||
use PHPStan\Reflection\MissingMethodFromReflectionException; | ||
use PHPStan\Reflection\ReflectionProvider; | ||
use PHPStan\ShouldNotHappenException; | ||
use Symfony\Component\Messenger\Handler\MessageSubscriberInterface; | ||
use function class_exists; | ||
use function count; | ||
use function is_array; | ||
use function is_int; | ||
use function is_string; | ||
|
||
final class MessageMapFactory | ||
{ | ||
|
||
private const MESSENGER_HANDLER_TAG = 'messenger.message_handler'; | ||
private const DEFAULT_HANDLER_METHOD = '__invoke'; | ||
|
||
/** @var ReflectionProvider */ | ||
private $reflectionProvider; | ||
|
||
/** @var ServiceMap */ | ||
private $serviceMap; | ||
|
||
public function __construct(ServiceMap $symfonyServiceMap, ReflectionProvider $reflectionProvider) | ||
{ | ||
$this->serviceMap = $symfonyServiceMap; | ||
$this->reflectionProvider = $reflectionProvider; | ||
} | ||
|
||
public function create(): MessageMap | ||
{ | ||
$returnTypesMap = []; | ||
|
||
foreach ($this->serviceMap->getServices() as $service) { | ||
$serviceClass = $service->getClass(); | ||
|
||
if ($serviceClass === null) { | ||
continue; | ||
} | ||
|
||
foreach ($service->getTags() as $tag) { | ||
if ($tag->getName() !== self::MESSENGER_HANDLER_TAG) { | ||
continue; | ||
} | ||
|
||
/** @var array{handles?: class-string, method?: string} $tagAttributes */ | ||
$tagAttributes = $tag->getAttributes(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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? |
||
$reflectionClass = $this->reflectionProvider->getClass($serviceClass); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is unsafe. Ask for hasClass first. |
||
|
||
if (isset($tagAttributes['handles'])) { | ||
$handles = [$tagAttributes['handles'] => ['method' => $tagAttributes['method'] ?? self::DEFAULT_HANDLER_METHOD]]; | ||
} else { | ||
$handles = $this->guessHandledMessages($reflectionClass); | ||
} | ||
|
||
foreach ($handles as $messageClassName => $options) { | ||
$methodReflection = $reflectionClass->getNativeMethod($options['method'] ?? self::DEFAULT_HANDLER_METHOD); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is unsafe. Ask for hasNativeMethod first. |
||
|
||
foreach ($methodReflection->getVariants() as $variant) { | ||
$returnTypesMap[$messageClassName][] = $variant->getReturnType(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
$messageMap = []; | ||
foreach ($returnTypesMap as $messageClassName => $returnTypes) { | ||
if (count($returnTypes) !== 1) { | ||
continue; | ||
} | ||
|
||
$messageMap[$messageClassName] = $returnTypes[0]; | ||
} | ||
|
||
return new MessageMap($messageMap); | ||
} | ||
|
||
/** @return array<class-string, array<string, string>> */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not return an array. It's a generator. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right, changed to iterable :) |
||
private function guessHandledMessages(ClassReflection $reflectionClass): iterable | ||
{ | ||
if ($reflectionClass->implementsInterface(MessageSubscriberInterface::class)) { | ||
foreach ($reflectionClass->getName()::getHandledMessages() as $index => $value) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Save the class name into a variable first, then call the method. |
||
if (self::containOptions($index, $value)) { | ||
yield $index => $value; | ||
} else { | ||
yield $value => ['method' => self::DEFAULT_HANDLER_METHOD]; | ||
} | ||
} | ||
|
||
return; | ||
} | ||
|
||
try { | ||
$methodReflection = $reflectionClass->getNativeMethod(self::DEFAULT_HANDLER_METHOD); | ||
} catch (MissingMethodFromReflectionException $e) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ask for hasNativeMethod first, don't catch the exception. |
||
return; | ||
} | ||
|
||
$variants = $methodReflection->getVariants(); | ||
if (count($variants) !== 1) { | ||
return; | ||
} | ||
|
||
$parameters = $variants[0]->getParameters(); | ||
|
||
if (count($parameters) !== 1) { | ||
return; | ||
} | ||
|
||
/** @var class-string[] $classNames */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't add this |
||
$classNames = $parameters[0]->getType()->getObjectClassNames(); | ||
|
||
if (count($classNames) !== 1) { | ||
return; | ||
} | ||
|
||
yield $classNames[0] => ['method' => self::DEFAULT_HANDLER_METHOD]; | ||
} | ||
|
||
/** | ||
* @param mixed $index | ||
* @param mixed $value | ||
* @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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of these asserts need |
||
*/ | ||
private static function containOptions($index, $value): bool | ||
{ | ||
if (is_string($index) && class_exists($index) && is_array($value)) { | ||
return true; | ||
} elseif (is_int($index) && is_string($value) && class_exists($value)) { | ||
return false; | ||
} | ||
|
||
throw new ShouldNotHappenException(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd say this can happen very easily. Wrong getHandledMessages implementation should not crash PHPStan. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, so now I'm catching that case and skipping when wrongly implemented method. |
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Symfony; | ||
|
||
final class ServiceTag implements ServiceTagDefinition | ||
{ | ||
|
||
/** @var string */ | ||
private $name; | ||
|
||
/** @var array<string, string> */ | ||
private $attributes; | ||
|
||
/** @param array<string, string> $attributes */ | ||
public function __construct(string $name, array $attributes = []) | ||
{ | ||
$this->name = $name; | ||
$this->attributes = $attributes; | ||
} | ||
|
||
public function getName(): string | ||
{ | ||
return $this->name; | ||
} | ||
|
||
public function getAttributes(): array | ||
{ | ||
return $this->attributes; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Symfony; | ||
|
||
interface ServiceTagDefinition | ||
{ | ||
|
||
public function getName(): string; | ||
|
||
/** @return array<string, string> */ | ||
public function getAttributes(): array; | ||
|
||
} |
There was a problem hiding this comment.
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.