From 7194ae0ae9ead02d91cfb91c533ab6044e495bc7 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 14 Oct 2026 17:08:37 +0200 Subject: [PATCH] Skip #[Route] attribute actions in NoPublicNonActionMethodInControllerRule --- ...oPublicNonActionMethodInControllerRule.php | 24 +++++++++++++++---- .../Fixture/SkipRouteAttributeController.php | 21 ++++++++++++++++ ...licNonActionMethodInControllerRuleTest.php | 1 + 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 tests/Rules/Symfony/NoPublicNonActionMethodInControllerRule/Fixture/SkipRouteAttributeController.php diff --git a/src/Rules/Symfony/NoPublicNonActionMethodInControllerRule.php b/src/Rules/Symfony/NoPublicNonActionMethodInControllerRule.php index 529204ec..c8e03ad6 100644 --- a/src/Rules/Symfony/NoPublicNonActionMethodInControllerRule.php +++ b/src/Rules/Symfony/NoPublicNonActionMethodInControllerRule.php @@ -11,13 +11,14 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Contracts\Service\Attribute\Required; /** * Only route actions - "*Action()" and "__invoke()" - can be public in a controller. * - * Magic methods, #[Required] setters, methods required by a parent class or interface and event subscriber - * listeners are skipped. + * Magic methods, #[Route] actions, #[Required] setters, methods required by a parent class or interface and + * event subscriber listeners are skipped. * * @see \Symplify\PHPStanRules\Tests\Rules\Symfony\NoPublicNonActionMethodInControllerRule\NoPublicNonActionMethodInControllerRuleTest * @@ -45,6 +46,14 @@ final class NoPublicNonActionMethodInControllerRule implements Rule */ private const REQUIRED_ATTRIBUTE = Required::class; + /** + * @var string[] + */ + private const ROUTE_ATTRIBUTES = [ + Route::class, + \Symfony\Component\Routing\Annotation\Route::class, + ]; + /** * @var string */ @@ -93,7 +102,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($this->hasRequiredAttribute($node) || $this->isInheritedContract($classReflection, $methodName)) { + if ($this->hasSkippedAttribute($node) || $this->isInheritedContract($classReflection, $methodName)) { return []; } @@ -106,11 +115,16 @@ public function processNode(Node $node, Scope $scope): array ]; } - private function hasRequiredAttribute(ClassMethod $classMethod): bool + private function hasSkippedAttribute(ClassMethod $classMethod): bool { foreach ($classMethod->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { - if ($attr->name->toString() === self::REQUIRED_ATTRIBUTE) { + $attributeName = $attr->name->toString(); + if ($attributeName === self::REQUIRED_ATTRIBUTE) { + return true; + } + + if (in_array($attributeName, self::ROUTE_ATTRIBUTES, true)) { return true; } } diff --git a/tests/Rules/Symfony/NoPublicNonActionMethodInControllerRule/Fixture/SkipRouteAttributeController.php b/tests/Rules/Symfony/NoPublicNonActionMethodInControllerRule/Fixture/SkipRouteAttributeController.php new file mode 100644 index 00000000..48d995c1 --- /dev/null +++ b/tests/Rules/Symfony/NoPublicNonActionMethodInControllerRule/Fixture/SkipRouteAttributeController.php @@ -0,0 +1,21 @@ +