Created
April 23, 2025 17:27
-
-
Save Neirda24/692985a44c7b9d797f5f9209d4007149 to your computer and use it in GitHub Desktop.
Rector rule to add the `@extends AbstractType<TData>` on forms based on `data_class`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Utils\Rector\Rector; | |
use PhpParser\Comment\Doc; | |
use PhpParser\Node; | |
use PhpParser\Node\Stmt\Class_; | |
use PhpParser\Node\Expr\Array_; | |
use PhpParser\Node\Expr\ArrayItem; | |
use PhpParser\Node\Expr\ClassConstFetch; | |
use PhpParser\Node\Scalar\String_; | |
use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; | |
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; | |
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; | |
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; | |
use Rector\PhpParser\Node\BetterNodeFinder; | |
use Rector\Rector\AbstractRector; | |
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; | |
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; | |
use PHPStan\Type\ObjectType; | |
use Symfony\Component\Form\AbstractType; | |
final class AddGenericToAbstractTypeRector extends AbstractRector | |
{ | |
public function __construct( | |
private readonly BetterNodeFinder $betterNodeFinder, | |
private readonly PhpDocInfoFactory $phpDocInfoFactory, | |
) { | |
} | |
public function getRuleDefinition(): RuleDefinition | |
{ | |
return new RuleDefinition( | |
'Add @extends AbstractType<Foo> on Symfony form types based on data_class option', | |
[ | |
new CodeSample( | |
<<<'CODE' | |
class MyFormType extends AbstractType | |
{ | |
public function configureOptions(OptionsResolver $resolver): void | |
{ | |
$resolver->setDefaults([ | |
'data_class' => Foo::class, | |
]); | |
} | |
} | |
CODE, | |
<<<'CODE' | |
/** | |
* @extends AbstractType<Foo> | |
*/ | |
class MyFormType extends AbstractType | |
{ | |
public function configureOptions(OptionsResolver $resolver): void | |
{ | |
$resolver->setDefaults([ | |
'data_class' => Foo::class, | |
]); | |
} | |
} | |
CODE | |
), | |
] | |
); | |
} | |
/** | |
* @return array<class-string<Node>> | |
*/ | |
public function getNodeTypes(): array | |
{ | |
return [Class_::class]; | |
} | |
/** | |
* @param \PhpParser\Node\Stmt\Class_ $node | |
*/ | |
public function refactor(Node $node): ?Node | |
{ | |
if (! $node instanceof Class_) { | |
return null; | |
} | |
if (! $this->isObjectType($node, new ObjectType(AbstractType::class))) { | |
return null; | |
} | |
$dataClass = $this->resolveDataClass($node); | |
if ($dataClass === null) { | |
return null; | |
} | |
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); | |
if ($phpDocInfo->hasByName('extends')) { | |
return null; | |
} | |
$phpDocInfo->addTagValueNode( | |
new ExtendsTagValueNode( | |
new GenericTypeNode( | |
new IdentifierTypeNode('AbstractType'), | |
[new IdentifierTypeNode($dataClass)], | |
), | |
'' | |
) | |
); | |
$node->setDocComment(new Doc($phpDocInfo->getPhpDocNode()->__toString())); | |
return $node; | |
} | |
private function resolveDataClass(Class_ $class): ?string | |
{ | |
foreach ($class->getMethods() as $method) { | |
if ($this->getName($method) !== 'configureOptions') { | |
continue; | |
} | |
foreach ($method->getStmts() ?? [] as $stmt) { | |
$methodCall = $this->betterNodeFinder->findFirstInstanceOf($stmt, \PhpParser\Node\Expr\MethodCall::class); | |
if (! $methodCall || ! $this->isName($methodCall->name, 'setDefaults')) { | |
continue; | |
} | |
$args = $methodCall->args; | |
if (! isset($args[0]) || ! $args[0]->value instanceof Array_) { | |
continue; | |
} | |
foreach ($args[0]->value->items as $item) { | |
if (! $item instanceof ArrayItem || ! $item->key instanceof String_) { | |
continue; | |
} | |
if ($item->key->value === 'data_class' && $item->value instanceof ClassConstFetch) { | |
return $this->getName($item->value->class); | |
} | |
} | |
} | |
} | |
return null; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Utils\Rector\Tests\Rector\AddGenericToAbstractTypeRector; | |
use Iterator; | |
use PHPUnit\Framework\Attributes\DataProvider; | |
use Rector\Testing\PHPUnit\AbstractRectorTestCase; | |
final class AddGenericToAbstractTypeRectorTest extends AbstractRectorTestCase | |
{ | |
#[DataProvider('provideData')] | |
public function test(string $filePath): void | |
{ | |
$this->doTestFile($filePath); | |
} | |
public static function provideData(): Iterator | |
{ | |
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); | |
} | |
public function provideConfigFilePath(): string | |
{ | |
return __DIR__ . '/config/configured_rule.php'; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use Rector\Config\RectorConfig; | |
use Utils\Rector\Rector\AddGenericToAbstractTypeRector; | |
return static function (RectorConfig $rectorConfig): void { | |
$rectorConfig->rule(AddGenericToAbstractTypeRector::class); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Utils\Rector\Tests\Rector\AddGenericToAbstractTypeRector\Fixture; | |
use Symfony\Component\Form\AbstractType; | |
use Symfony\Component\OptionsResolver\OptionsResolver; | |
use Some\Foo; | |
class MyFormType extends AbstractType | |
{ | |
public function configureOptions(OptionsResolver $resolver): void | |
{ | |
$resolver->setDefaults([ | |
'data_class' => Foo::class, | |
]); | |
} | |
} | |
?> | |
----- | |
<?php | |
namespace Utils\Rector\Tests\Rector\AddGenericToAbstractTypeRector\Fixture; | |
use Symfony\Component\Form\AbstractType; | |
use Symfony\Component\OptionsResolver\OptionsResolver; | |
use Some\Foo; | |
/** | |
* @extends AbstractType<Some\Foo> | |
*/ | |
class MyFormType extends AbstractType | |
{ | |
public function configureOptions(OptionsResolver $resolver): void | |
{ | |
$resolver->setDefaults([ | |
'data_class' => Foo::class, | |
]); | |
} | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment