diff --git a/README.md b/README.md index 7e3116c..5f0ccda 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,76 @@ final class Alert
+### ForbiddenInheritanceRule + +Forbids the use of class inheritance in Twig Components. Composition via traits should be used instead. +This promotes better code reusability and avoids tight coupling between components. + +> [!TIP] +> 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, +> for example `` instead of ``. + +```yaml +rules: + - Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ForbiddenInheritanceRule +``` + +```php +// src/Twig/Components/Alert.php +namespace App\Twig\Components; + +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; + +abstract class BaseComponent +{ + public string $name; +} + +#[AsTwigComponent] +final class Alert extends BaseComponent +{ +} +``` + +:x: + +
+ +```php +// src/Twig/Components/Alert.php +namespace App\Twig\Components; + +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; + +trait CommonComponentTrait +{ + public string $name; +} + +#[AsTwigComponent] +final class Alert +{ + use CommonComponentTrait; +} +``` + +```php +// src/Twig/Components/Alert.php +namespace App\Twig\Components; + +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; + +#[AsTwigComponent] +final class Alert +{ + public string $name; +} +``` + +:+1: + +
+ ### PublicPropertiesShouldBeCamelCaseRule Enforces that all public properties in Twig Components follow camelCase naming convention. diff --git a/src/Rules/TwigComponent/ForbiddenInheritanceRule.php b/src/Rules/TwigComponent/ForbiddenInheritanceRule.php new file mode 100644 index 0000000..4ae1bd5 --- /dev/null +++ b/src/Rules/TwigComponent/ForbiddenInheritanceRule.php @@ -0,0 +1,43 @@ + + */ +final class ForbiddenInheritanceRule implements Rule +{ + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (! AttributeFinder::findAttribute($node, AsTwigComponent::class)) { + return []; + } + + if ($node->extends !== null) { + return [ + RuleErrorBuilder::message('Using class inheritance in a Twig component is forbidden, use traits for composition instead.') + ->identifier('symfonyUX.twigComponent.forbiddenClassInheritance') + ->line($node->extends->getLine()) + ->tip('Consider using traits to share common functionality between Twig components.') + ->build(), + ]; + } + + return []; + } +} diff --git a/tests/Rules/TwigComponent/ForbiddenInheritanceRule/Fixture/ComponentUsingTrait.php b/tests/Rules/TwigComponent/ForbiddenInheritanceRule/Fixture/ComponentUsingTrait.php new file mode 100644 index 0000000..68732a3 --- /dev/null +++ b/tests/Rules/TwigComponent/ForbiddenInheritanceRule/Fixture/ComponentUsingTrait.php @@ -0,0 +1,18 @@ + + */ +final class ForbiddenInheritanceRuleTest extends RuleTestCase +{ + public function testViolations(): void + { + $this->analyse( + [__DIR__ . '/Fixture/ComponentWithInheritance.php'], + [ + [ + 'Using class inheritance in a Twig component is forbidden, use traits for composition instead.', + 15, + 'Consider using traits to share common functionality between Twig components.', + ], + ] + ); + } + + public function testNoViolations(): void + { + $this->analyse( + [__DIR__ . '/Fixture/NotAComponent.php'], + [] + ); + + $this->analyse( + [__DIR__ . '/Fixture/ComponentWithoutInheritance.php'], + [] + ); + + $this->analyse( + [__DIR__ . '/Fixture/ComponentUsingTrait.php'], + [] + ); + } + + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/configured_rule.neon']; + } + + protected function getRule(): Rule + { + return self::getContainer()->getByType(ForbiddenInheritanceRule::class); + } +} diff --git a/tests/Rules/TwigComponent/ForbiddenInheritanceRule/config/configured_rule.neon b/tests/Rules/TwigComponent/ForbiddenInheritanceRule/config/configured_rule.neon new file mode 100644 index 0000000..f43631d --- /dev/null +++ b/tests/Rules/TwigComponent/ForbiddenInheritanceRule/config/configured_rule.neon @@ -0,0 +1,2 @@ +rules: + - Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ForbiddenInheritanceRule