From 8db1436ce7567a45827f3dd7abff2888ec921624 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 29 Sep 2026 23:51:03 +0200 Subject: [PATCH] Skip Symfony-only rules that would misfire on Laravel projects --- config/symfony-rules.neon | 2 ++ src/NodeAnalyzer/LaravelPresenceResolver.php | 25 +++++++++++++++++++ .../NoConstructorAndRequiredTogetherRule.php | 12 ++++++++- .../Symfony/NoListenerWithoutContractRule.php | 12 ++++++++- .../NoNullableServiceInConstructorRule.php | 6 +++++ src/Rules/Symfony/NoServiceJugglingRule.php | 12 ++++++++- .../LaravelPresenceResolverTest.php | 19 ++++++++++++++ ...ConstructorAndRequiredTogetherRuleTest.php | 5 +++- .../NoListenerWithoutContractRuleTest.php | 5 +++- ...NoNullableServiceInConstructorRuleTest.php | 6 ++++- .../NoServiceJugglingRuleTest.php | 5 +++- 11 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 src/NodeAnalyzer/LaravelPresenceResolver.php create mode 100644 tests/NodeAnalyzer/LaravelPresenceResolverTest.php diff --git a/config/symfony-rules.neon b/config/symfony-rules.neon index a52203e57..ad7561295 100644 --- a/config/symfony-rules.neon +++ b/config/symfony-rules.neon @@ -55,6 +55,8 @@ conditionalTags: phpstan.rules.rule: %symplify.symfony% services: + - Symplify\PHPStanRules\NodeAnalyzer\LaravelPresenceResolver + - Symplify\PHPStanRules\Rules\Symfony\SingleArgEventDispatchRule - Symplify\PHPStanRules\Rules\Symfony\NoListenerWithoutContractRule - Symplify\PHPStanRules\Rules\Symfony\NoStringInGetSubscribedEventsRule diff --git a/src/NodeAnalyzer/LaravelPresenceResolver.php b/src/NodeAnalyzer/LaravelPresenceResolver.php new file mode 100644 index 000000000..42e4852de --- /dev/null +++ b/src/NodeAnalyzer/LaravelPresenceResolver.php @@ -0,0 +1,25 @@ +reflectionProvider->hasClass(self::LARAVEL_APPLICATION_CLASS); + } +} diff --git a/src/Rules/Symfony/NoConstructorAndRequiredTogetherRule.php b/src/Rules/Symfony/NoConstructorAndRequiredTogetherRule.php index 070b33b8d..0b6e7d02a 100644 --- a/src/Rules/Symfony/NoConstructorAndRequiredTogetherRule.php +++ b/src/Rules/Symfony/NoConstructorAndRequiredTogetherRule.php @@ -13,16 +13,22 @@ use PHPStan\Rules\RuleErrorBuilder; use Symplify\PHPStanRules\Enum\MethodName; use Symplify\PHPStanRules\Enum\RuleIdentifier\SymfonyRuleIdentifier; +use Symplify\PHPStanRules\NodeAnalyzer\LaravelPresenceResolver; /** * @implements Rule * * @see \Symplify\PHPStanRules\Tests\Rules\Symfony\NoConstructorAndRequiredTogetherRule\NoConstructorAndRequiredTogetherRuleTest */ -final class NoConstructorAndRequiredTogetherRule implements Rule +final readonly class NoConstructorAndRequiredTogetherRule implements Rule { public const string ERROR_MESSAGE = 'Avoid using __construct() and @required in the same class. Pick one to keep architecture clean'; + public function __construct( + private LaravelPresenceResolver $laravelPresenceResolver, + ) { + } + public function getNodeType(): string { return Class_::class; @@ -34,6 +40,10 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { + if ($this->laravelPresenceResolver->isLaravelProject()) { + return []; + } + if ($node->isAnonymous()) { return []; } diff --git a/src/Rules/Symfony/NoListenerWithoutContractRule.php b/src/Rules/Symfony/NoListenerWithoutContractRule.php index bcacf0d11..f66db514e 100644 --- a/src/Rules/Symfony/NoListenerWithoutContractRule.php +++ b/src/Rules/Symfony/NoListenerWithoutContractRule.php @@ -15,6 +15,7 @@ use Symplify\PHPStanRules\Doctrine\DoctrineEventSubscriberAnalyzer; use Symplify\PHPStanRules\Enum\RuleIdentifier\SymfonyRuleIdentifier; use Symplify\PHPStanRules\Enum\SymfonyClass; +use Symplify\PHPStanRules\NodeAnalyzer\LaravelPresenceResolver; /** * Based on https://tomasvotruba.com/blog/2019/07/22/how-to-convert-listeners-to-subscribers-and-reduce-your-configs @@ -24,10 +25,15 @@ * * @see \Symplify\PHPStanRules\Tests\Rules\Symfony\NoListenerWithoutContractRule\NoListenerWithoutContractRuleTest */ -final class NoListenerWithoutContractRule implements Rule +final readonly class NoListenerWithoutContractRule implements Rule { public const string ERROR_MESSAGE = 'There should be no listeners modified in config. Use EventSubscriberInterface contract or #[AsEventListener] attribute and native PHP instead'; + public function __construct( + private LaravelPresenceResolver $laravelPresenceResolver, + ) { + } + public function getNodeType(): string { return InClassNode::class; @@ -38,6 +44,10 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { + if ($this->laravelPresenceResolver->isLaravelProject()) { + return []; + } + if (! $scope->isInClass()) { return []; } diff --git a/src/Rules/Symfony/NoNullableServiceInConstructorRule.php b/src/Rules/Symfony/NoNullableServiceInConstructorRule.php index 9bb62f71d..761aa1998 100644 --- a/src/Rules/Symfony/NoNullableServiceInConstructorRule.php +++ b/src/Rules/Symfony/NoNullableServiceInConstructorRule.php @@ -20,6 +20,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use Symplify\PHPStanRules\Enum\RuleIdentifier; +use Symplify\PHPStanRules\NodeAnalyzer\LaravelPresenceResolver; use Throwable; /** @@ -63,6 +64,7 @@ public function __construct( private ReflectionProvider $reflectionProvider, + private LaravelPresenceResolver $laravelPresenceResolver, ) { } @@ -78,6 +80,10 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { + if ($this->laravelPresenceResolver->isLaravelProject()) { + return []; + } + if ($node->name->toLowerString() !== '__construct') { return []; } diff --git a/src/Rules/Symfony/NoServiceJugglingRule.php b/src/Rules/Symfony/NoServiceJugglingRule.php index 376c43d3b..f0898fa0a 100644 --- a/src/Rules/Symfony/NoServiceJugglingRule.php +++ b/src/Rules/Symfony/NoServiceJugglingRule.php @@ -26,6 +26,7 @@ use PHPStan\Rules\RuleErrorBuilder; use Symfony\Contracts\Service\Attribute\Required; use Symplify\PHPStanRules\Enum\RuleIdentifier; +use Symplify\PHPStanRules\NodeAnalyzer\LaravelPresenceResolver; /** * A service injected in __construct() or an autowire*() method must not be handed over to another method call. @@ -38,7 +39,7 @@ * * @implements Rule */ -final class NoServiceJugglingRule implements Rule +final readonly class NoServiceJugglingRule implements Rule { public const string ERROR_MESSAGE = 'Service "$this->%s" is passed to "%s()" as an argument. Inject "%s" in the constructor of the class that uses it instead'; @@ -48,6 +49,11 @@ final class NoServiceJugglingRule implements Rule private const string REQUIRED_ATTRIBUTE = Required::class; + public function __construct( + private LaravelPresenceResolver $laravelPresenceResolver, + ) { + } + public function getNodeType(): string { return InClassNode::class; @@ -60,6 +66,10 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { + if ($this->laravelPresenceResolver->isLaravelProject()) { + return []; + } + $classLike = $node->getOriginalNode(); if (! $classLike instanceof Class_) { return []; diff --git a/tests/NodeAnalyzer/LaravelPresenceResolverTest.php b/tests/NodeAnalyzer/LaravelPresenceResolverTest.php new file mode 100644 index 000000000..eb9da9038 --- /dev/null +++ b/tests/NodeAnalyzer/LaravelPresenceResolverTest.php @@ -0,0 +1,19 @@ +createReflectionProvider()); + + // Laravel is not installed here, so it must not be detected + $this->assertFalse($laravelPresenceResolver->isLaravelProject()); + } +} diff --git a/tests/Rules/Symfony/NoConstructorAndRequiredTogetherRule/NoConstructorAndRequiredTogetherRuleTest.php b/tests/Rules/Symfony/NoConstructorAndRequiredTogetherRule/NoConstructorAndRequiredTogetherRuleTest.php index e9d7213ff..2c967a683 100644 --- a/tests/Rules/Symfony/NoConstructorAndRequiredTogetherRule/NoConstructorAndRequiredTogetherRuleTest.php +++ b/tests/Rules/Symfony/NoConstructorAndRequiredTogetherRule/NoConstructorAndRequiredTogetherRuleTest.php @@ -8,6 +8,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; +use Symplify\PHPStanRules\NodeAnalyzer\LaravelPresenceResolver; use Symplify\PHPStanRules\Rules\Symfony\NoConstructorAndRequiredTogetherRule; final class NoConstructorAndRequiredTogetherRuleTest extends RuleTestCase @@ -39,6 +40,8 @@ public static function provideData(): Iterator protected function getRule(): Rule { - return new NoConstructorAndRequiredTogetherRule(); + return new NoConstructorAndRequiredTogetherRule( + new LaravelPresenceResolver($this->createReflectionProvider()) + ); } } diff --git a/tests/Rules/Symfony/NoListenerWithoutContractRule/NoListenerWithoutContractRuleTest.php b/tests/Rules/Symfony/NoListenerWithoutContractRule/NoListenerWithoutContractRuleTest.php index 8066ae32d..9ce403ba4 100644 --- a/tests/Rules/Symfony/NoListenerWithoutContractRule/NoListenerWithoutContractRuleTest.php +++ b/tests/Rules/Symfony/NoListenerWithoutContractRule/NoListenerWithoutContractRuleTest.php @@ -8,6 +8,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; +use Symplify\PHPStanRules\NodeAnalyzer\LaravelPresenceResolver; use Symplify\PHPStanRules\Rules\Symfony\NoListenerWithoutContractRule; final class NoListenerWithoutContractRuleTest extends RuleTestCase @@ -46,6 +47,8 @@ public static function provideData(): Iterator protected function getRule(): Rule { - return new NoListenerWithoutContractRule(); + return new NoListenerWithoutContractRule( + new LaravelPresenceResolver($this->createReflectionProvider()) + ); } } diff --git a/tests/Rules/Symfony/NoNullableServiceInConstructorRule/NoNullableServiceInConstructorRuleTest.php b/tests/Rules/Symfony/NoNullableServiceInConstructorRule/NoNullableServiceInConstructorRuleTest.php index 69cfc409f..c1f017e82 100644 --- a/tests/Rules/Symfony/NoNullableServiceInConstructorRule/NoNullableServiceInConstructorRuleTest.php +++ b/tests/Rules/Symfony/NoNullableServiceInConstructorRule/NoNullableServiceInConstructorRuleTest.php @@ -8,6 +8,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; +use Symplify\PHPStanRules\NodeAnalyzer\LaravelPresenceResolver; use Symplify\PHPStanRules\Rules\Symfony\NoNullableServiceInConstructorRule; use Symplify\PHPStanRules\Tests\Rules\Symfony\NoNullableServiceInConstructorRule\Source\AnotherService; use Symplify\PHPStanRules\Tests\Rules\Symfony\NoNullableServiceInConstructorRule\Source\SomeService; @@ -55,6 +56,9 @@ public static function provideData(): Iterator protected function getRule(): Rule { - return new NoNullableServiceInConstructorRule($this->createReflectionProvider()); + return new NoNullableServiceInConstructorRule( + $this->createReflectionProvider(), + new LaravelPresenceResolver($this->createReflectionProvider()) + ); } } diff --git a/tests/Rules/Symfony/NoServiceJugglingRule/NoServiceJugglingRuleTest.php b/tests/Rules/Symfony/NoServiceJugglingRule/NoServiceJugglingRuleTest.php index 6fa142268..5e246a2b8 100644 --- a/tests/Rules/Symfony/NoServiceJugglingRule/NoServiceJugglingRuleTest.php +++ b/tests/Rules/Symfony/NoServiceJugglingRule/NoServiceJugglingRuleTest.php @@ -8,6 +8,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; +use Symplify\PHPStanRules\NodeAnalyzer\LaravelPresenceResolver; use Symplify\PHPStanRules\Rules\Symfony\NoServiceJugglingRule; use Symplify\PHPStanRules\Tests\Rules\Symfony\NoServiceJugglingRule\Source\SomeUserHelper; @@ -48,6 +49,8 @@ public static function provideData(): Iterator protected function getRule(): Rule { - return new NoServiceJugglingRule(); + return new NoServiceJugglingRule( + new LaravelPresenceResolver($this->createReflectionProvider()) + ); } }