Skip to content

Commit e32ae70

Browse files
committed
Add ClassMustBeFinalRule and remove ForbiddenInheritanceRule
1 parent a381ba7 commit e32ae70

19 files changed

+244
-242
lines changed

README.md

Lines changed: 57 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,63 @@ Each rule can be enabled individually by adding it to your `phpstan.dist.neon` c
2121
> [!NOTE]
2222
> All these rules also apply to LiveComponents (classes annotated with `#[AsLiveComponent]`).
2323
24+
### ClassMustBeFinalRule
25+
26+
Enforces that all Twig Component classes must be declared as `final`.
27+
This prevents inheritance and promotes composition via traits, ensuring better code maintainability and avoiding tight coupling between components.
28+
29+
```yaml
30+
rules:
31+
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ClassMustBeFinalRule
32+
```
33+
34+
```php
35+
// src/Twig/Components/Alert.php
36+
namespace App\Twig\Components;
37+
38+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
39+
40+
#[AsTwigComponent]
41+
class Alert
42+
{
43+
public string $message;
44+
}
45+
```
46+
47+
```php
48+
// src/Twig/Components/Alert.php
49+
namespace App\Twig\Components;
50+
51+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
52+
53+
#[AsTwigComponent]
54+
abstract class Alert
55+
{
56+
public string $message;
57+
}
58+
```
59+
60+
:x:
61+
62+
<br>
63+
64+
```php
65+
// src/Twig/Components/Alert.php
66+
namespace App\Twig\Components;
67+
68+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
69+
70+
#[AsTwigComponent]
71+
final class Alert
72+
{
73+
public string $message;
74+
}
75+
```
76+
77+
:+1:
78+
79+
<br>
80+
2481
### ClassNameShouldNotEndWithComponentRule
2582

2683
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.
@@ -230,76 +287,6 @@ final class Alert
230287

231288
<br>
232289

233-
### ForbiddenInheritanceRule
234-
235-
Forbids the use of class inheritance in Twig Components. Composition via traits should be used instead.
236-
This promotes better code reusability and avoids tight coupling between components.
237-
238-
> [!TIP]
239-
> Another alternative is to use [Class Variant Authority](https://symfony.com/bundles/ux-twig-component/current/index.html#component-with-complex-variants-cva) to create variations of a base component without inheritance or traits,
240-
> for example `<twig:Alert variant="success"></twig:Alert>` instead of `<twig:AlertSuccess></twig:AlertSuccess>`.
241-
242-
```yaml
243-
rules:
244-
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ForbiddenInheritanceRule
245-
```
246-
247-
```php
248-
// src/Twig/Components/Alert.php
249-
namespace App\Twig\Components;
250-
251-
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
252-
253-
abstract class BaseComponent
254-
{
255-
public string $name;
256-
}
257-
258-
#[AsTwigComponent]
259-
class Alert extends BaseComponent
260-
{
261-
}
262-
```
263-
264-
:x:
265-
266-
<br>
267-
268-
```php
269-
// src/Twig/Components/Alert.php
270-
namespace App\Twig\Components;
271-
272-
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
273-
274-
trait CommonComponentTrait
275-
{
276-
public string $name;
277-
}
278-
279-
#[AsTwigComponent]
280-
final class Alert
281-
{
282-
use CommonComponentTrait;
283-
}
284-
```
285-
286-
```php
287-
// src/Twig/Components/Alert.php
288-
namespace App\Twig\Components;
289-
290-
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
291-
292-
#[AsTwigComponent]
293-
final class Alert
294-
{
295-
public string $name;
296-
}
297-
```
298-
299-
:+1:
300-
301-
<br>
302-
303290
### MethodsShouldBePublicOrPrivateRule
304291

305292
Enforces that all methods in Twig Components are either public or private, but not protected.

src/Rules/TwigComponent/ForbiddenInheritanceRule.php renamed to src/Rules/TwigComponent/ClassMustBeFinalRule.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
/**
1717
* @implements Rule<Class_>
1818
*/
19-
final class ForbiddenInheritanceRule implements Rule
19+
final class ClassMustBeFinalRule implements Rule
2020
{
2121
public function getNodeType(): string
2222
{
@@ -29,12 +29,20 @@ public function processNode(Node $node, Scope $scope): array
2929
return [];
3030
}
3131

32-
if ($node->extends !== null) {
32+
if ($node->isAbstract()) {
3333
return [
34-
RuleErrorBuilder::message('Using class inheritance in a Twig component is forbidden, use traits for composition instead.')
35-
->identifier('symfonyUX.twigComponent.forbiddenClassInheritance')
36-
->line($node->extends->getLine())
37-
->tip('Consider using traits to share common functionality between Twig components.')
34+
RuleErrorBuilder::message('Twig component class must be final, not abstract.')
35+
->identifier('symfonyUX.twigComponent.classMustBeFinal')
36+
->tip('Make the class final and use traits for composition instead of inheritance.')
37+
->build(),
38+
];
39+
}
40+
41+
if (! $node->isFinal()) {
42+
return [
43+
RuleErrorBuilder::message('Twig component class must be final.')
44+
->identifier('symfonyUX.twigComponent.classMustBeFinal')
45+
->tip('Add the "final" keyword to the class declaration to prevent inheritance.')
3846
->build(),
3947
];
4048
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ClassMustBeFinalRule;
6+
7+
use Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ClassMustBeFinalRule;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
11+
/**
12+
* @extends RuleTestCase<ClassMustBeFinalRule>
13+
*/
14+
final class ClassMustBeFinalRuleTest extends RuleTestCase
15+
{
16+
public function testViolations(): void
17+
{
18+
$this->analyse(
19+
[__DIR__ . '/Fixture/InvalidNonFinalTwigComponent.php'],
20+
[
21+
[
22+
'Twig component class must be final.',
23+
9,
24+
'Add the "final" keyword to the class declaration to prevent inheritance.',
25+
],
26+
]
27+
);
28+
29+
$this->analyse(
30+
[__DIR__ . '/Fixture/InvalidNonFinalLiveComponent.php'],
31+
[
32+
[
33+
'Twig component class must be final.',
34+
9,
35+
'Add the "final" keyword to the class declaration to prevent inheritance.',
36+
],
37+
]
38+
);
39+
40+
$this->analyse(
41+
[__DIR__ . '/Fixture/InvalidAbstractTwigComponent.php'],
42+
[
43+
[
44+
'Twig component class must be final, not abstract.',
45+
9,
46+
'Make the class final and use traits for composition instead of inheritance.',
47+
],
48+
]
49+
);
50+
51+
$this->analyse(
52+
[__DIR__ . '/Fixture/InvalidAbstractLiveComponent.php'],
53+
[
54+
[
55+
'Twig component class must be final, not abstract.',
56+
9,
57+
'Make the class final and use traits for composition instead of inheritance.',
58+
],
59+
]
60+
);
61+
}
62+
63+
public function testNoViolations(): void
64+
{
65+
$this->analyse(
66+
[__DIR__ . '/Fixture/NotAComponent.php'],
67+
[]
68+
);
69+
70+
$this->analyse(
71+
[__DIR__ . '/Fixture/ValidTwigComponent.php'],
72+
[]
73+
);
74+
75+
$this->analyse(
76+
[__DIR__ . '/Fixture/ValidLiveComponent.php'],
77+
[]
78+
);
79+
}
80+
81+
public static function getAdditionalConfigFiles(): array
82+
{
83+
return [__DIR__ . '/config/configured_rule.neon'];
84+
}
85+
86+
protected function getRule(): Rule
87+
{
88+
return self::getContainer()->getByType(ClassMustBeFinalRule::class);
89+
}
90+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ClassMustBeFinalRule\Fixture;
6+
7+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
8+
9+
#[AsLiveComponent]
10+
abstract class InvalidAbstractLiveComponent
11+
{
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ClassMustBeFinalRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
9+
#[AsTwigComponent]
10+
abstract class InvalidAbstractTwigComponent
11+
{
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ClassMustBeFinalRule\Fixture;
6+
7+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
8+
9+
#[AsLiveComponent]
10+
class InvalidNonFinalLiveComponent
11+
{
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ClassMustBeFinalRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
9+
#[AsTwigComponent]
10+
class InvalidNonFinalTwigComponent
11+
{
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ClassMustBeFinalRule\Fixture;
6+
7+
class NotAComponent
8+
{
9+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ClassMustBeFinalRule\Fixture;
6+
7+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
8+
9+
#[AsLiveComponent]
10+
final class ValidLiveComponent
11+
{
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ClassMustBeFinalRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
9+
#[AsTwigComponent]
10+
final class ValidTwigComponent
11+
{
12+
}

0 commit comments

Comments
 (0)