Skip to content

Instantly share code, notes, and snippets.

@ktarila
Created December 31, 2024 15:50
Show Gist options
  • Save ktarila/5a34e17e570c79e078bf323a9291d174 to your computer and use it in GitHub Desktop.
Save ktarila/5a34e17e570c79e078bf323a9291d174 to your computer and use it in GitHub Desktop.
Rector rule to fix symfony 7.1 automatic mapping of route parameters into Doctrine entities deprecation
<?php
declare(strict_types=1);
namespace Utils\Rector\Rector;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Identifier;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Rector\AbstractRector;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\Routing\Attribute\Route;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
/**
* @see \Rector\Tests\TypeDeclaration\Rector\AddMapEntityRector\AddMapEntityRectorTest
*/
final class AddMapEntityRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Add #[MapEntity] attribute to parameters based on Route ID.', [
new CodeSample(
<<<'CODE_SAMPLE'
#[Route('/user/{id}', name: 'user_show', methods: ['GET'])]
public function showUser(
User $user
): Response {
// Controller logic
}
CODE_SAMPLE,
<<<'CODE_SAMPLE'
#[Route('/user/{id}', name: 'user_show', methods: ['GET'])]
public function showUser(
#[MapEntity(id: 'id')] User $user
): Response {
// Controller logic
}
CODE_SAMPLE
),
]);
}
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
// @todo select node type
return [ClassMethod::class];
}
/**
* @param \PhpParser\Node\Stmt\Class_ $node
*/
public function refactor(Node $node): ?Node
{
// Check if the method has a #[Route] attribute
$routeAttribute = null;
foreach ($node->getAttrGroups() as $attrGroup) {
foreach ($attrGroup->attrs as $attribute) {
if ($this->isName($attribute->name, Route::class)) {
$routeAttribute = $attribute;
break;
}
}
}
if (!$routeAttribute) {
return null;
}
// Extract the placeholder from the Route path
$routePath = $routeAttribute->args[0]->value;
preg_match('/\{(\w+)\}/', $routePath->value, $matches);
$routeParameterName = $matches[1] ?? null;
if (!$routeParameterName || 'id' !== $routeParameterName) {
return null;
}
// Add #[MapEntity] to parameters matching the route placeholder
foreach ($node->params as $param) {
if ($param instanceof Param && $this->isAppEntity($param->type)) {
if ($this->hasMapEntityAttribute($param)) {
break;
}
// Create the MapEntity attribute directly as #[MapEntity(id: 'id')]
$args = [new Arg(new String_('id'), name: new Identifier('id'))];
$mapEntityAttribute = new Attribute(
new Node\Name\FullyQualified(MapEntity::class),
$args
);
$param->attrGroups[] = new AttributeGroup([$mapEntityAttribute]);
break;
}
}
return $node;
}
private function isAppEntity(?Node $type): bool
{
if (null === $type) {
return false;
}
return str_starts_with($this->getName($type) ?? '', 'App\Entity\\');
}
private function hasMapEntityAttribute(Param $param): bool
{
// Check if the parameter has the #[MapEntity] attribute
foreach ($param->attrGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attribute) {
if ($this->isName($attribute->name, MapEntity::class)) {
return true; // The attribute exists
}
}
}
return false; // The attribute does not exist
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment