Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,76 @@ final class Alert

<br>

### 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 `<twig:Alert variant="success"></twig:Alert>` instead of `<twig:AlertSuccess></twig:AlertSuccess>`.

```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:

<br>

```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:

<br>

### PublicPropertiesShouldBeCamelCaseRule

Enforces that all public properties in Twig Components follow camelCase naming convention.
Expand Down
43 changes: 43 additions & 0 deletions src/Rules/TwigComponent/ForbiddenInheritanceRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Rules\TwigComponent;

use Kocal\PHPStanSymfonyUX\NodeAnalyzer\AttributeFinder;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

/**
* @implements Rule<Class_>
*/
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 [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ForbiddenInheritanceRule\Fixture;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

trait CommonComponentTrait
{
public string $name;
}

#[AsTwigComponent]
final class ComponentUsingTrait
{
use CommonComponentTrait;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ForbiddenInheritanceRule\Fixture;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

abstract class BaseComponent
{
public string $name;
}

#[AsTwigComponent]
final class ComponentWithInheritance extends BaseComponent
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ForbiddenInheritanceRule\Fixture;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent]
final class ComponentWithoutInheritance
{
public string $name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ForbiddenInheritanceRule\Fixture;

abstract class BaseClass
{
public string $name;
}

final class NotAComponent extends BaseClass
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ForbiddenInheritanceRule;

use Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ForbiddenInheritanceRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ForbiddenInheritanceRule>
*/
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rules:
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ForbiddenInheritanceRule