Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Neirda24/692985a44c7b9d797f5f9209d4007149 to your computer and use it in GitHub Desktop.
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`
<?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;
}
}
<?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';
}
}
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Utils\Rector\Rector\AddGenericToAbstractTypeRector;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(AddGenericToAbstractTypeRector::class);
};
<?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