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