Skip to content

Commit

Permalink
[Symfony] Add FormTypeClassNameRule to require clear naming for form …
Browse files Browse the repository at this point in the history
…types (#169)
  • Loading branch information
TomasVotruba authored Feb 5, 2025
1 parent 97c5818 commit f1fc3d2
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 0 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,17 @@ final class SomeFixture extends AbstractFixture

## 3. Symfony-specific Rules

### FormTypeClassNameRule

```yaml
rules:
- Symplify\PHPStanRules\Rules\Symfony\FormTypeClassNameRule
```

Classes that extend `AbstractType` should have `*FormType` suffix, to make it clear it's a form class.

<br>

### NoConstructorAndRequiredTogetherRule

Constructor injection and `#[Required]` method should not be used together in single class. Pick one, to keep architecture clean.
Expand Down
1 change: 1 addition & 0 deletions config/symfony-rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ rules:
- Symplify\PHPStanRules\Rules\Symfony\NoListenerWithoutContractRule
- Symplify\PHPStanRules\Rules\Symfony\NoStringInGetSubscribedEventsRule
- Symplify\PHPStanRules\Rules\Symfony\RequireInvokableControllerRule
- Symplify\PHPStanRules\Rules\Symfony\FormTypeClassNameRule

# dependency injection
- Symplify\PHPStanRules\Rules\Symfony\NoGetDoctrineInControllerRule
Expand Down
5 changes: 5 additions & 0 deletions src/Enum/ClassName.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ final class ClassName
*/
public const FORM_EVENTS = 'Symfony\Component\Form\FormEvents';

/**
* @var string
*/
public const FORM_TYPE = 'Symfony\Component\Form\AbstractType';

/**
* @var string
*/
Expand Down
2 changes: 2 additions & 0 deletions src/Enum/SymfonyRuleIdentifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ final class SymfonyRuleIdentifier
public const SYMFONY_REQUIRED_ONLY_IN_ABSTRACT = 'symfony.requiredOnlyInAbstract';

public const NO_CONSTRUCT_AND_REQUIRED = 'symfony.noConstructAndRequired';

public const FORM_TYPE_CLASS_NAME = 'symfony.formTypeClassName';
}
65 changes: 65 additions & 0 deletions src/Rules/Symfony/FormTypeClassNameRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Rules\Symfony;

use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;
use Symplify\PHPStanRules\Enum\ClassName;
use Symplify\PHPStanRules\Enum\SymfonyRuleIdentifier;

/**
* @implements Rule<Class_>
*
* @see \Symplify\PHPStanRules\Tests\Rules\Symfony\FormTypeClassNameRule\FormTypeClassNameRuleTest
*/
final class FormTypeClassNameRule implements Rule
{
public function getNodeType(): string
{
return Class_::class;
}

/**
* @param Class_ $node
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $node->namespacedName instanceof Name) {
return [];
}

// all good
$className = $node->namespacedName->toString();
if (str_ends_with($className, 'FormType')) {
return [];
}

$currentObjectType = new ObjectType($className);

$parentObjectType = new ObjectType(ClassName::FORM_TYPE);
if (! $parentObjectType->isSuperTypeOf($currentObjectType)->yes()) {
return [];
}

$errorMessage = sprintf(
'Class extends "%s" must have "FormType" suffix to make form explicit, "%s" given',
ClassName::FORM_TYPE,
$className
);

$identifierRuleError = RuleErrorBuilder::message($errorMessage)
->identifier(SymfonyRuleIdentifier::FORM_TYPE_CLASS_NAME)
->build();

return [$identifierRuleError];
}
}
11 changes: 11 additions & 0 deletions tests/Rules/Symfony/FormTypeClassNameRule/Fixture/SomeFormType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\Symfony\FormTypeClassNameRule\Fixture;

use Symfony\Component\Form\AbstractType;

final class SomeFormType extends AbstractType
{
}
11 changes: 11 additions & 0 deletions tests/Rules/Symfony/FormTypeClassNameRule/Fixture/SomeType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\Symfony\FormTypeClassNameRule\Fixture;

use Symfony\Component\Form\AbstractType;

final class SomeType extends AbstractType
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\Symfony\FormTypeClassNameRule;

use Iterator;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use Symplify\PHPStanRules\Rules\Symfony\FormTypeClassNameRule;

final class FormTypeClassNameRuleTest extends RuleTestCase
{
/**
* @param mixed[] $expectedErrorMessagesWithLines
*/
#[DataProvider('provideData')]
public function testRule(string $filePath, array $expectedErrorMessagesWithLines): void
{
$this->analyse([$filePath], $expectedErrorMessagesWithLines);
}

public static function provideData(): Iterator
{
yield [__DIR__ . '/Fixture/SomeFormType.php', []];

yield [__DIR__ . '/Fixture/SomeType.php', [
[
'Class extends "Symfony\Component\Form\AbstractType" must have "FormType" suffix to make form explicit, "Symplify\PHPStanRules\Tests\Rules\Symfony\FormTypeClassNameRule\Fixture\SomeType" given',
9,
],
]];
}

protected function getRule(): Rule
{
return new FormTypeClassNameRule();
}
}

0 comments on commit f1fc3d2

Please sign in to comment.