diff --git a/README.md b/README.md index 4484e28..9a5e2cc 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,63 @@ final class Alert
+### ExposePublicPropsShouldBeFalseRule + +Enforces that the `#[AsTwigComponent]` attribute has its `exposePublicProps` parameter explicitly set to `false`. +This prevents public properties from being automatically exposed to templates, promoting explicit control over what data is accessible in your Twig components. + +```yaml +rules: + - Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ExposePublicPropsShouldBeFalseRule +``` + +```php +// src/Twig/Components/Alert.php +namespace App\Twig\Components; + +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; + +#[AsTwigComponent] +final class Alert +{ + public string $message; +} +``` + +```php +// src/Twig/Components/Alert.php +namespace App\Twig\Components; + +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; + +#[AsTwigComponent(exposePublicProps: true)] +final class Alert +{ + public string $message; +} +``` + +:x: + +
+ +```php +// src/Twig/Components/Alert.php +namespace App\Twig\Components; + +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; + +#[AsTwigComponent(exposePublicProps: false)] +final class Alert +{ + public string $message; +} +``` + +:+1: + +
+ ### ForbiddenAttributesPropertyRule Forbid the use of the `$attributes` property in Twig Components, which can lead to confusion when using `{{ attributes }}` (an instance of `ComponentAttributes` that is automatically injected) in Twig templates. diff --git a/src/Rules/TwigComponent/ExposePublicPropsShouldBeFalseRule.php b/src/Rules/TwigComponent/ExposePublicPropsShouldBeFalseRule.php new file mode 100644 index 0000000..952ddee --- /dev/null +++ b/src/Rules/TwigComponent/ExposePublicPropsShouldBeFalseRule.php @@ -0,0 +1,64 @@ + + */ +final class ExposePublicPropsShouldBeFalseRule implements Rule +{ + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (! $asTwigComponent = AttributeFinder::findAttribute($node, AsTwigComponent::class)) { + return []; + } + + $exposePublicPropsValue = $this->getExposePublicPropsValue($asTwigComponent); + + if ($exposePublicPropsValue !== false) { + return [ + RuleErrorBuilder::message('The #[AsTwigComponent] attribute must have its "exposePublicProps" parameter set to false.') + ->identifier('symfonyUX.twigComponent.exposePublicPropsShouldBeFalse') + ->line($asTwigComponent->getLine()) + ->tip('Set "exposePublicProps" to false in the #[AsTwigComponent] attribute.') + ->build(), + ]; + } + + return []; + } + + private function getExposePublicPropsValue(Node\Attribute $attribute): ?bool + { + foreach ($attribute->args as $arg) { + if ($arg->name && $arg->name->toString() === 'exposePublicProps') { + if ($arg->value instanceof Node\Expr\ConstFetch) { + $constantName = $arg->value->name->toString(); + + return match (strtolower($constantName)) { + 'true' => true, + 'false' => false, + default => null, + }; + } + } + } + + return null; + } +} diff --git a/tests/Rules/TwigComponent/ExposePublicPropsShouldBeFalseRule/ExposePublicPropsShouldBeFalseRuleTest.php b/tests/Rules/TwigComponent/ExposePublicPropsShouldBeFalseRule/ExposePublicPropsShouldBeFalseRuleTest.php new file mode 100644 index 0000000..5ab3291 --- /dev/null +++ b/tests/Rules/TwigComponent/ExposePublicPropsShouldBeFalseRule/ExposePublicPropsShouldBeFalseRuleTest.php @@ -0,0 +1,63 @@ + + */ +final class ExposePublicPropsShouldBeFalseRuleTest extends RuleTestCase +{ + public function testViolations(): void + { + $this->analyse( + [__DIR__ . '/Fixture/ComponentWithoutExposePublicProps.php'], + [ + [ + 'The #[AsTwigComponent] attribute must have its "exposePublicProps" parameter set to false.', + 9, + 'Set "exposePublicProps" to false in the #[AsTwigComponent] attribute.', + ], + ] + ); + + $this->analyse( + [__DIR__ . '/Fixture/ComponentWithExposePublicPropsTrue.php'], + [ + [ + 'The #[AsTwigComponent] attribute must have its "exposePublicProps" parameter set to false.', + 9, + 'Set "exposePublicProps" to false in the #[AsTwigComponent] attribute.', + ], + ] + ); + } + + public function testNoViolations(): void + { + $this->analyse( + [__DIR__ . '/Fixture/NotAComponent.php'], + [] + ); + + $this->analyse( + [__DIR__ . '/Fixture/ComponentWithExposePublicPropsFalse.php'], + [] + ); + } + + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/configured_rule.neon']; + } + + protected function getRule(): Rule + { + return self::getContainer()->getByType(ExposePublicPropsShouldBeFalseRule::class); + } +} diff --git a/tests/Rules/TwigComponent/ExposePublicPropsShouldBeFalseRule/Fixture/ComponentWithExposePublicPropsFalse.php b/tests/Rules/TwigComponent/ExposePublicPropsShouldBeFalseRule/Fixture/ComponentWithExposePublicPropsFalse.php new file mode 100644 index 0000000..db83ebb --- /dev/null +++ b/tests/Rules/TwigComponent/ExposePublicPropsShouldBeFalseRule/Fixture/ComponentWithExposePublicPropsFalse.php @@ -0,0 +1,13 @@ +