Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<br>

```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:

<br>

## TwigComponent Rules

> [!NOTE]
Expand Down
54 changes: 54 additions & 0 deletions src/Rules/LiveComponent/LiveActionMethodsShouldBePublicRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Rules\LiveComponent;

use Kocal\PHPStanSymfonyUX\NodeAnalyzer\AttributeFinder;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;

/**
* @implements Rule<Class_>
*/
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Tests\Rules\LiveComponent\LiveActionMethodsShouldBePublicRule\Fixture;

use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;

#[AsLiveComponent]
final class LiveComponentWithPrivateLiveAction
{
public string $name = '';

#[LiveAction]
private function save(): void
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Tests\Rules\LiveComponent\LiveActionMethodsShouldBePublicRule\Fixture;

use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;

#[AsLiveComponent]
final class LiveComponentWithProtectedLiveAction
{
public string $email = '';

#[LiveAction]
protected function delete(): void
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Tests\Rules\LiveComponent\LiveActionMethodsShouldBePublicRule\Fixture;

use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;

#[AsLiveComponent]
final class LiveComponentWithPublicLiveAction
{
public string $title = '';

#[LiveAction]
public function submit(): void
{
}

#[LiveAction]
public function cancel(): void
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Tests\Rules\LiveComponent\LiveActionMethodsShouldBePublicRule\Fixture;

use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;

#[AsLiveComponent]
final class LiveComponentWithoutLiveAction
{
public string $count = '0';

public function increment(): void
{
}

private function helperMethod(): void
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Tests\Rules\LiveComponent\LiveActionMethodsShouldBePublicRule\Fixture;

class NotAComponent
{
private function save(): void
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Tests\Rules\LiveComponent\LiveActionMethodsShouldBePublicRule;

use Kocal\PHPStanSymfonyUX\Rules\LiveComponent\LiveActionMethodsShouldBePublicRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<LiveActionMethodsShouldBePublicRule>
*/
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rules:
- Kocal\PHPStanSymfonyUX\Rules\LiveComponent\LiveActionMethodsShouldBePublicRule