Skip to content

Commit f1fc3d2

Browse files
authored
[Symfony] Add FormTypeClassNameRule to require clear naming for form types (#169)
1 parent 97c5818 commit f1fc3d2

File tree

8 files changed

+146
-0
lines changed

8 files changed

+146
-0
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,17 @@ final class SomeFixture extends AbstractFixture
11771177

11781178
## 3. Symfony-specific Rules
11791179

1180+
### FormTypeClassNameRule
1181+
1182+
```yaml
1183+
rules:
1184+
- Symplify\PHPStanRules\Rules\Symfony\FormTypeClassNameRule
1185+
```
1186+
1187+
Classes that extend `AbstractType` should have `*FormType` suffix, to make it clear it's a form class.
1188+
1189+
<br>
1190+
11801191
### NoConstructorAndRequiredTogetherRule
11811192

11821193
Constructor injection and `#[Required]` method should not be used together in single class. Pick one, to keep architecture clean.

config/symfony-rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ rules:
55
- Symplify\PHPStanRules\Rules\Symfony\NoListenerWithoutContractRule
66
- Symplify\PHPStanRules\Rules\Symfony\NoStringInGetSubscribedEventsRule
77
- Symplify\PHPStanRules\Rules\Symfony\RequireInvokableControllerRule
8+
- Symplify\PHPStanRules\Rules\Symfony\FormTypeClassNameRule
89

910
# dependency injection
1011
- Symplify\PHPStanRules\Rules\Symfony\NoGetDoctrineInControllerRule

src/Enum/ClassName.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ final class ClassName
6161
*/
6262
public const FORM_EVENTS = 'Symfony\Component\Form\FormEvents';
6363

64+
/**
65+
* @var string
66+
*/
67+
public const FORM_TYPE = 'Symfony\Component\Form\AbstractType';
68+
6469
/**
6570
* @var string
6671
*/

src/Enum/SymfonyRuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@ final class SymfonyRuleIdentifier
2727
public const SYMFONY_REQUIRED_ONLY_IN_ABSTRACT = 'symfony.requiredOnlyInAbstract';
2828

2929
public const NO_CONSTRUCT_AND_REQUIRED = 'symfony.noConstructAndRequired';
30+
31+
public const FORM_TYPE_CLASS_NAME = 'symfony.formTypeClassName';
3032
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Symfony;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Name;
9+
use PhpParser\Node\Stmt\Class_;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleError;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use PHPStan\Type\ObjectType;
15+
use Symplify\PHPStanRules\Enum\ClassName;
16+
use Symplify\PHPStanRules\Enum\SymfonyRuleIdentifier;
17+
18+
/**
19+
* @implements Rule<Class_>
20+
*
21+
* @see \Symplify\PHPStanRules\Tests\Rules\Symfony\FormTypeClassNameRule\FormTypeClassNameRuleTest
22+
*/
23+
final class FormTypeClassNameRule implements Rule
24+
{
25+
public function getNodeType(): string
26+
{
27+
return Class_::class;
28+
}
29+
30+
/**
31+
* @param Class_ $node
32+
* @return RuleError[]
33+
*/
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
if (! $node->namespacedName instanceof Name) {
37+
return [];
38+
}
39+
40+
// all good
41+
$className = $node->namespacedName->toString();
42+
if (str_ends_with($className, 'FormType')) {
43+
return [];
44+
}
45+
46+
$currentObjectType = new ObjectType($className);
47+
48+
$parentObjectType = new ObjectType(ClassName::FORM_TYPE);
49+
if (! $parentObjectType->isSuperTypeOf($currentObjectType)->yes()) {
50+
return [];
51+
}
52+
53+
$errorMessage = sprintf(
54+
'Class extends "%s" must have "FormType" suffix to make form explicit, "%s" given',
55+
ClassName::FORM_TYPE,
56+
$className
57+
);
58+
59+
$identifierRuleError = RuleErrorBuilder::message($errorMessage)
60+
->identifier(SymfonyRuleIdentifier::FORM_TYPE_CLASS_NAME)
61+
->build();
62+
63+
return [$identifierRuleError];
64+
}
65+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\FormTypeClassNameRule\Fixture;
6+
7+
use Symfony\Component\Form\AbstractType;
8+
9+
final class SomeFormType extends AbstractType
10+
{
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\FormTypeClassNameRule\Fixture;
6+
7+
use Symfony\Component\Form\AbstractType;
8+
9+
final class SomeType extends AbstractType
10+
{
11+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\FormTypeClassNameRule;
6+
7+
use Iterator;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use Symplify\PHPStanRules\Rules\Symfony\FormTypeClassNameRule;
12+
13+
final class FormTypeClassNameRuleTest extends RuleTestCase
14+
{
15+
/**
16+
* @param mixed[] $expectedErrorMessagesWithLines
17+
*/
18+
#[DataProvider('provideData')]
19+
public function testRule(string $filePath, array $expectedErrorMessagesWithLines): void
20+
{
21+
$this->analyse([$filePath], $expectedErrorMessagesWithLines);
22+
}
23+
24+
public static function provideData(): Iterator
25+
{
26+
yield [__DIR__ . '/Fixture/SomeFormType.php', []];
27+
28+
yield [__DIR__ . '/Fixture/SomeType.php', [
29+
[
30+
'Class extends "Symfony\Component\Form\AbstractType" must have "FormType" suffix to make form explicit, "Symplify\PHPStanRules\Tests\Rules\Symfony\FormTypeClassNameRule\Fixture\SomeType" given',
31+
9,
32+
],
33+
]];
34+
}
35+
36+
protected function getRule(): Rule
37+
{
38+
return new FormTypeClassNameRule();
39+
}
40+
}

0 commit comments

Comments
 (0)