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 @@
+