diff --git a/README.md b/README.md index 6bbd7fe..d5afb11 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,82 @@ After installing the package, you need to configure PHPStan to use the rules. Each rule can be enabled individually by adding it to your `phpstan.dist.neon` configuration file. +## LiveComponent Rules + +### LiveActionMethodsShouldBePublicRule + +Enforces that all methods annotated with `#[LiveAction]` in LiveComponents must be declared as public. +LiveAction methods need to be publicly accessible to be invoked as component actions from the frontend. + +```yaml +rules: + - Kocal\PHPStanSymfonyUX\Rules\LiveComponent\LiveActionMethodsShouldBePublicRule +``` + +```php +// src/Twig/Components/TodoList.php +namespace App\Twig\Components; + +use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\Attribute\LiveAction; + +#[AsLiveComponent] +final class TodoList +{ + #[LiveAction] + private function addItem(): void + { + } +} +``` + +```php +// src/Twig/Components/TodoList.php +namespace App\Twig\Components; + +use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\Attribute\LiveAction; + +#[AsLiveComponent] +final class TodoList +{ + #[LiveAction] + protected function deleteItem(): void + { + } +} +``` + +:x: + +
+ +```php +// src/Twig/Components/TodoList.php +namespace App\Twig\Components; + +use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\Attribute\LiveAction; + +#[AsLiveComponent] +final class TodoList +{ + #[LiveAction] + public function addItem(): void + { + } + + #[LiveAction] + public function deleteItem(): void + { + } +} +``` + +:+1: + +
+ ## TwigComponent Rules > [!NOTE] diff --git a/src/Rules/LiveComponent/LiveActionMethodsShouldBePublicRule.php b/src/Rules/LiveComponent/LiveActionMethodsShouldBePublicRule.php new file mode 100644 index 0000000..6637b8a --- /dev/null +++ b/src/Rules/LiveComponent/LiveActionMethodsShouldBePublicRule.php @@ -0,0 +1,54 @@ + + */ +final class LiveActionMethodsShouldBePublicRule implements Rule +{ + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (! AttributeFinder::findAnyAttribute($node, [AsLiveComponent::class])) { + return []; + } + + $errors = []; + + foreach ($node->getMethods() as $method) { + if (! AttributeFinder::findAnyAttribute($method, [LiveAction::class])) { + continue; + } + + if (! $method->isPublic()) { + $methodName = $method->name->toString(); + + $errors[] = RuleErrorBuilder::message( + sprintf('LiveAction method "%s()" should be public.', $methodName) + ) + ->identifier('symfonyUX.liveComponent.liveActionMethodsShouldBePublic') + ->line($method->getLine()) + ->tip('Methods annotated with #[LiveAction] must be public to be accessible as component actions.') + ->build(); + } + } + + return $errors; + } +} diff --git a/tests/Rules/LiveComponent/LiveActionMethodsShouldBePublicRule/Fixture/LiveComponentWithPrivateLiveAction.php b/tests/Rules/LiveComponent/LiveActionMethodsShouldBePublicRule/Fixture/LiveComponentWithPrivateLiveAction.php new file mode 100644 index 0000000..bfb5c25 --- /dev/null +++ b/tests/Rules/LiveComponent/LiveActionMethodsShouldBePublicRule/Fixture/LiveComponentWithPrivateLiveAction.php @@ -0,0 +1,19 @@ + + */ +final class LiveActionMethodsShouldBePublicRuleTest extends RuleTestCase +{ + public function testViolations(): void + { + $this->analyse( + [__DIR__ . '/Fixture/LiveComponentWithPrivateLiveAction.php'], + [ + [ + 'LiveAction method "save()" should be public.', + 15, + 'Methods annotated with #[LiveAction] must be public to be accessible as component actions.', + ], + ] + ); + + $this->analyse( + [__DIR__ . '/Fixture/LiveComponentWithProtectedLiveAction.php'], + [ + [ + 'LiveAction method "delete()" should be public.', + 15, + 'Methods annotated with #[LiveAction] must be public to be accessible as component actions.', + ], + ] + ); + } + + public function testNoViolations(): void + { + $this->analyse( + [__DIR__ . '/Fixture/NotAComponent.php'], + [] + ); + + $this->analyse( + [__DIR__ . '/Fixture/LiveComponentWithPublicLiveAction.php'], + [] + ); + + $this->analyse( + [__DIR__ . '/Fixture/LiveComponentWithoutLiveAction.php'], + [] + ); + } + + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/configured_rule.neon']; + } + + protected function getRule(): Rule + { + return self::getContainer()->getByType(LiveActionMethodsShouldBePublicRule::class); + } +} diff --git a/tests/Rules/LiveComponent/LiveActionMethodsShouldBePublicRule/config/configured_rule.neon b/tests/Rules/LiveComponent/LiveActionMethodsShouldBePublicRule/config/configured_rule.neon new file mode 100644 index 0000000..3f69b1c --- /dev/null +++ b/tests/Rules/LiveComponent/LiveActionMethodsShouldBePublicRule/config/configured_rule.neon @@ -0,0 +1,2 @@ +rules: + - Kocal\PHPStanSymfonyUX\Rules\LiveComponent\LiveActionMethodsShouldBePublicRule