Skip to content

Commit 2e0a553

Browse files
SymfonyUsageProvider: Add AsMessageHandler Annotation for Symfony
1 parent f32746a commit 2e0a553

File tree

3 files changed

+122
-0
lines changed

3 files changed

+122
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ $ vendor/bin/phpstan
4242
- constructors, calls, factory methods
4343
- [`phpstan/phpstan-symfony`](https://github.com/phpstan/phpstan-symfony) with `containerXmlPath` must be used
4444
- `#[AsEventListener]` attribute
45+
- `#[AsMessageHandler]` attribute
4546
- `#[AsController]` attribute
4647
- `#[AsCommand]` attribute
4748
- `#[Required]` attribute

src/Provider/SymfonyUsageProvider.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,10 @@ protected function shouldMarkAsUsed(ReflectionMethod $method): ?string
363363
return 'Event listener method via #[AsEventListener] attribute';
364364
}
365365

366+
if ($this->isMessageHandlerMethodWithAsMessageHandlerAttribute($method)) {
367+
return 'Message handler method via #[AsMessageHandler] attribute';
368+
}
369+
366370
if ($this->isWorkflowEventListenerMethod($method)) {
367371
return 'Workflow event listener method via workflow attribute';
368372
}
@@ -500,6 +504,49 @@ protected function isEventListenerMethodWithAsEventListenerAttribute(ReflectionM
500504
|| $this->hasAttribute($method, 'Symfony\Component\EventDispatcher\Attribute\AsEventListener');
501505
}
502506

507+
protected function isMessageHandlerMethodWithAsMessageHandlerAttribute(ReflectionMethod $method): bool
508+
{
509+
$class = $method->getDeclaringClass();
510+
$methodName = $method->getName();
511+
512+
// Check if this method has the attribute directly (fallback to method name itself if no target specified)
513+
foreach ($method->getAttributes('Symfony\Component\Messenger\Attribute\AsMessageHandler') as $attribute) {
514+
$arguments = $attribute->getArguments();
515+
$targetMethod = $arguments['method'] ?? $arguments[3] ?? $methodName;
516+
517+
if ($targetMethod === $methodName) {
518+
return true;
519+
}
520+
}
521+
522+
// Check class-level attributes (fallback to __invoke if no target specified)
523+
foreach ($class->getAttributes('Symfony\Component\Messenger\Attribute\AsMessageHandler') as $attribute) {
524+
$arguments = $attribute->getArguments();
525+
$targetMethod = $arguments['method'] ?? $arguments[3] ?? '__invoke';
526+
527+
if ($targetMethod === $methodName) {
528+
return true;
529+
}
530+
}
531+
532+
// Check if any other method points to this method (only if explicitly specified)
533+
foreach ($class->getMethods() as $otherMethod) {
534+
if ($otherMethod->getName() === $methodName) {
535+
continue;
536+
}
537+
538+
foreach ($otherMethod->getAttributes('Symfony\Component\Messenger\Attribute\AsMessageHandler') as $attribute) {
539+
$arguments = $attribute->getArguments();
540+
$targetMethod = $arguments['method'] ?? $arguments[3] ?? null;
541+
if ($methodName === $targetMethod) {
542+
return true;
543+
}
544+
}
545+
}
546+
547+
return false;
548+
}
549+
503550
protected function isWorkflowEventListenerMethod(ReflectionMethod $method): bool
504551
{
505552
return $this->hasAttribute($method, 'Symfony\Component\Workflow\Attribute\AsAnnounceListener')

tests/Rule/data/providers/symfony-gte71.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
66
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
7+
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
78
use Symfony\Component\Routing\Attribute\Route;
89
use Symfony\Component\Workflow\Attribute\AsAnnounceListener;
910
use Symfony\Component\Workflow\Attribute\AsCompletedListener;
@@ -100,3 +101,76 @@ public function deadMethod(): void // error: Unused Symfony\WorkflowEventListene
100101
{
101102
}
102103
}
104+
105+
// Test AsMessageHandler with default __invoke method
106+
#[AsMessageHandler]
107+
class MessageHandlerWithInvoke
108+
{
109+
public function __invoke(): void
110+
{
111+
}
112+
113+
public function deadMethod(): void // error: Unused Symfony\MessageHandlerWithInvoke::deadMethod
114+
{
115+
}
116+
}
117+
118+
// Test AsMessageHandler with custom method
119+
#[AsMessageHandler(method: 'handleMessage')]
120+
class MessageHandlerWithCustomMethod
121+
{
122+
public function handleMessage(): void
123+
{
124+
}
125+
126+
public function deadMethod(): void // error: Unused Symfony\MessageHandlerWithCustomMethod::deadMethod
127+
{
128+
}
129+
}
130+
131+
// Test AsMessageHandler on method level without parameters
132+
class MessageHandlerWithMethodAttribute
133+
{
134+
#[AsMessageHandler]
135+
public function handleDirectly(): void
136+
{
137+
}
138+
139+
public function deadMethod(): void // error: Unused Symfony\MessageHandlerWithMethodAttribute::deadMethod
140+
{
141+
}
142+
}
143+
144+
// Test AsMessageHandler on method level with method parameter (edge case)
145+
class MessageHandlerWithMethodLevelRedirect
146+
{
147+
#[AsMessageHandler(null, null, null, 'actualHandler')]
148+
public function annotatedMethod(): void // error: Unused Symfony\MessageHandlerWithMethodLevelRedirect::annotatedMethod
149+
{
150+
}
151+
152+
public function actualHandler(): void
153+
{
154+
}
155+
156+
public function deadMethod(): void // error: Unused Symfony\MessageHandlerWithMethodLevelRedirect::deadMethod
157+
{
158+
}
159+
}
160+
161+
// Test AsMessageHandler on method level with named parameter (edge case)
162+
class MessageHandlerWithMethodLevelNamedRedirect
163+
{
164+
#[AsMessageHandler(method: 'realHandler')]
165+
public function annotatedMethod(): void // error: Unused Symfony\MessageHandlerWithMethodLevelNamedRedirect::annotatedMethod
166+
{
167+
}
168+
169+
public function realHandler(): void
170+
{
171+
}
172+
173+
public function deadMethod(): void // error: Unused Symfony\MessageHandlerWithMethodLevelNamedRedirect::deadMethod
174+
{
175+
}
176+
}

0 commit comments

Comments
 (0)