Skip to content

Commit c25c1a6

Browse files
committed
Add support for LiveComponents in PHPStan rules and documentation
1 parent 49fb964 commit c25c1a6

28 files changed

+321
-18
lines changed

AGENTS.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
## Project Overview
44

5-
This project contains custom PHPStan rules to improve static analysis of Symfony UX applications, particularly for Twig components.
5+
This project contains custom PHPStan rules to improve static analysis of Symfony UX applications, particularly for Twig components and Live components.
6+
7+
> [!NOTE]
8+
> All TwigComponent rules also apply to LiveComponents (classes annotated with `#[AsLiveComponent]`), since LiveComponents are enhanced TwigComponents.
69
710
## Project Structure
811

@@ -37,6 +40,7 @@ use PhpParser\Node\Stmt\Class_;
3740
use PHPStan\Analyser\Scope;
3841
use PHPStan\Rules\Rule;
3942
use PHPStan\Rules\RuleErrorBuilder;
43+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
4044
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
4145

4246
/**
@@ -51,7 +55,7 @@ final class MyRuleRule implements Rule
5155

5256
public function processNode(Node $node, Scope $scope): array
5357
{
54-
if (! AttributeFinder::findAttribute($node, AsTwigComponent::class)) {
58+
if (! AttributeFinder::findAnyAttribute($node, [AsTwigComponent::class, AsLiveComponent::class])) {
5559
return [];
5660
}
5761

@@ -149,9 +153,9 @@ rules:
149153
```
150154
151155
#### Fixtures:
152-
- **InvalidCase.php**: Example that violates the rule
153-
- **ValidCase.php**: Example that complies with the rule
154-
- **NotAComponent.php**: Class without `#[AsTwigComponent]` (should not trigger an error)
156+
- **InvalidCase.php**: Example that violates the rule (both for TwigComponent and LiveComponent)
157+
- **ValidCase.php**: Example that complies with the rule (both for TwigComponent and LiveComponent)
158+
- **NotAComponent.php**: Class without `#[AsTwigComponent]` or `#[AsLiveComponent]` (should not trigger an error)
155159

156160
### 3. Document the rule in `README.md`
157161

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# PHPStan for Symfony UX
22

3-
A set of PHPStan rules to improve static analysis for Symfony UX applications.
3+
A set of PHPStan rules to improve static analysis for [Symfony UX](https://github.com/symfony/ux) applications.
44

55
## Installation
66

@@ -10,8 +10,17 @@ To install the PHPStan rules for Symfony UX, you can use Composer:
1010
composer require --dev kocal/phpstan-symfony-ux
1111
```
1212

13+
## Configuration
14+
15+
After installing the package, you need to configure PHPStan to use the rules.
16+
17+
Each rule can be enabled individually by adding it to your `phpstan.dist.neon` configuration file.
18+
1319
## TwigComponent Rules
1420

21+
> [!NOTE]
22+
> All these rules also apply to LiveComponents (classes annotated with `#[AsLiveComponent]`), since LiveComponents are enhanced TwigComponents.
23+
1524
### ClassNameShouldNotEndWithComponentRule
1625

1726
Forbid Twig Component class names from ending with "Component" suffix, as it creates redundancy since the class is already identified as a component through the `#[AsTwigComponent]` attribute.

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
},
3737
"require-dev": {
3838
"phpunit/phpunit": "^11.1",
39+
"symfony/ux-live-component": "^2.0",
3940
"symfony/ux-twig-component": "^2.0",
4041
"symplify/easy-coding-standard": "^13.0"
4142
},

src/NodeAnalyzer/AttributeFinder.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,26 @@ public static function findAttribute(ClassMethod | Property | ClassLike | Param
4646

4747
return null;
4848
}
49+
50+
/**
51+
* Find any of the provided attributes.
52+
*
53+
* @param string[] $desiredAttributeClasses
54+
*/
55+
public static function findAnyAttribute(ClassMethod | Property | ClassLike | Param $node, array $desiredAttributeClasses): ?Attribute
56+
{
57+
$attributes = self::findAttributes($node);
58+
59+
foreach ($attributes as $attribute) {
60+
if (! $attribute->name instanceof FullyQualified) {
61+
continue;
62+
}
63+
64+
if (in_array($attribute->name->toString(), $desiredAttributeClasses, true)) {
65+
return $attribute;
66+
}
67+
}
68+
69+
return null;
70+
}
4971
}

src/Rules/TwigComponent/ClassNameShouldNotEndWithComponentRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Analyser\Scope;
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
13+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1314
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
1415

1516
/**
@@ -24,7 +25,7 @@ public function getNodeType(): string
2425

2526
public function processNode(Node $node, Scope $scope): array
2627
{
27-
if (! AttributeFinder::findAttribute($node, AsTwigComponent::class)) {
28+
if (! AttributeFinder::findAnyAttribute($node, [AsTwigComponent::class, AsLiveComponent::class])) {
2829
return [];
2930
}
3031

src/Rules/TwigComponent/ExposePublicPropsShouldBeFalseRule.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Analyser\Scope;
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
13+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1314
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
1415

1516
/**
@@ -24,18 +25,24 @@ public function getNodeType(): string
2425

2526
public function processNode(Node $node, Scope $scope): array
2627
{
27-
if (! $asTwigComponent = AttributeFinder::findAttribute($node, AsTwigComponent::class)) {
28+
if (! $attribute = AttributeFinder::findAnyAttribute($node, [AsTwigComponent::class, AsLiveComponent::class])) {
2829
return [];
2930
}
3031

31-
$exposePublicPropsValue = $this->getExposePublicPropsValue($asTwigComponent);
32+
$exposePublicPropsValue = $this->getExposePublicPropsValue($attribute);
3233

3334
if ($exposePublicPropsValue !== false) {
3435
return [
35-
RuleErrorBuilder::message('The #[AsTwigComponent] attribute must have its "exposePublicProps" parameter set to false.')
36+
RuleErrorBuilder::message(sprintf(
37+
'The #[%s] attribute must have its "exposePublicProps" parameter set to false.',
38+
$attribute->name->getLast(),
39+
))
3640
->identifier('symfonyUX.twigComponent.exposePublicPropsShouldBeFalse')
37-
->line($asTwigComponent->getLine())
38-
->tip('Set "exposePublicProps" to false in the #[AsTwigComponent] attribute.')
41+
->line($attribute->getLine())
42+
->tip(sprintf(
43+
'Set "exposePublicProps" to false in the #[%s] attribute.',
44+
$attribute->name->getLast()
45+
))
3946
->build(),
4047
];
4148
}

src/Rules/TwigComponent/ForbiddenAttributesPropertyRule.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Reflection\ReflectionProvider;
1212
use PHPStan\Rules\Rule;
1313
use PHPStan\Rules\RuleErrorBuilder;
14+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1415
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
1516

1617
/**
@@ -30,19 +31,19 @@ public function getNodeType(): string
3031

3132
public function processNode(Node $node, Scope $scope): array
3233
{
33-
if (! $asTwigComponent = AttributeFinder::findAttribute($node, AsTwigComponent::class)) {
34+
if (! $attribute = AttributeFinder::findAnyAttribute($node, [AsTwigComponent::class, AsLiveComponent::class])) {
3435
return [];
3536
}
3637

37-
if (! $attributesVarName = $this->getAttributesVarName($asTwigComponent)) {
38+
if (! $attributesVarName = $this->getAttributesVarName($attribute)) {
3839
return [];
3940
}
4041

4142
if ($propertyAttributes = $node->getProperty($attributesVarName['name'])) {
4243
return [
4344
RuleErrorBuilder::message(
4445
$attributesVarName['custom']
45-
? sprintf('Using property "%s" in a Twig component is forbidden, it may lead to confusion with the "%s" attribute defined in #[AsTwigComponent].', $attributesVarName['name'], $attributesVarName['name'])
46+
? sprintf('Using property "%s" in a Twig component is forbidden, it may lead to confusion with the "%s" attribute defined in #[%s].', $attributesVarName['name'], $attributesVarName['name'], $attribute->name->getLast())
4647
: sprintf('Using property "%s" in a Twig component is forbidden, it may lead to confusion with the default "attributes" Twig variable.', $attributesVarName['name'])
4748
)
4849
->identifier('symfonyUX.twigComponent.forbiddenAttributesProperty')

src/Rules/TwigComponent/ForbiddenClassPropertyRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Analyser\Scope;
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
13+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1314
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
1415

1516
/**
@@ -24,7 +25,7 @@ public function getNodeType(): string
2425

2526
public function processNode(Node $node, Scope $scope): array
2627
{
27-
if (! AttributeFinder::findAttribute($node, AsTwigComponent::class)) {
28+
if (! AttributeFinder::findAnyAttribute($node, [AsTwigComponent::class, AsLiveComponent::class])) {
2829
return [];
2930
}
3031

src/Rules/TwigComponent/ForbiddenInheritanceRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Analyser\Scope;
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
13+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1314
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
1415

1516
/**
@@ -24,7 +25,7 @@ public function getNodeType(): string
2425

2526
public function processNode(Node $node, Scope $scope): array
2627
{
27-
if (! AttributeFinder::findAttribute($node, AsTwigComponent::class)) {
28+
if (! AttributeFinder::findAnyAttribute($node, [AsTwigComponent::class, AsLiveComponent::class])) {
2829
return [];
2930
}
3031

src/Rules/TwigComponent/PublicPropertiesShouldBeCamelCaseRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Analyser\Scope;
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
13+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1314
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
1415

1516
/**
@@ -24,7 +25,7 @@ public function getNodeType(): string
2425

2526
public function processNode(Node $node, Scope $scope): array
2627
{
27-
if (! AttributeFinder::findAttribute($node, AsTwigComponent::class)) {
28+
if (! AttributeFinder::findAnyAttribute($node, [AsTwigComponent::class, AsLiveComponent::class])) {
2829
return [];
2930
}
3031

0 commit comments

Comments
 (0)