
Fixing Automatic Route Parameter Mapping Deprecation in Symfony Using a Custom Rector Rule
- Patrick Kenekayoro
- Symfony , Php
- December 31, 2024
Symfony 7.1 added alot of new features and a few deprecations such as deprecating automatic mapping of route parameters into Doctrine entities.
Manual Fix
To resolve this deprecation, Symfony recommends explicitly mapping route parameters to entities using the MapEntity
attribute. Here’s the example from the symfony blog https://symfony.com/blog/new-in-symfony-7-1-mapped-route-parameters
.
// Before
#[Route('/document/{id}')]
public function showDocument(Document $document): Response
// After
#[Route('/document/{id}')]
public function showDocument(
#[MapEntity(id: 'id')] Document $document,
): Response
While this fix is straightforward for a few controllers, manually updating large or medium-sized projects becomes a daunting task. Controllers generated by Symfony’s MakerBundle
often contain the deprecated pattern, meaning projects could have thousands of routes needing updates.
Automating the Fix with Rector
Rector is a powerful tool for automated code refactoring in PHP. While it doesn’t currently include a rule for this specific deprecation, we can create a custom Rector rule to handle it efficiently.
Step 1: Generate the Basic Rector Rule
First, generate the basic structure for your custom Rector rule:
vendor/bin/rector add-mapped-entity-rule
Step 2: Define the Rule
The core of your custom rule is defining the transformation: specifying outdated code patterns and their updated equivalents. Here’s an example for the #[MapEntity]
attribute:
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
),
]);
}
Step 3: Implement the Refactor Logic
The actual transformation logic is implemented in the refactor
function. Here’s how it works:
Identify the
#[Route]
Attribute:
The rule scans the controller method for a#[Route]
attribute.Extract the Placeholder from the Route Path:
Using a regular expression, it identifies placeholders like{id}
in the route path.Add the
#[MapEntity]
Attribute:
If the placeholder matches the parameter name (e.g.,id
), the rule adds a#[MapEntity]
attribute to the parameter.
Here’s the complete implementation of the refactor
function:
/**
* @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;
}
Step 4: Test and Integrate
Once the rule is implemented, you can test it on a small subset of your controllers to ensure it works as expected. You can run Rector with a configuration file that points to your custom rule.
Here’s an example of how to run Rector with a configuration file:
vendor/bin/rector process src/Controller --config rector.php
Checkout the - Github Gist: https://gist.github.com/ktarila/5a34e17e570c79e078bf323a9291d174 for the full code
Happy coding! 🚀