Fixing Automatic Route Parameter Mapping Deprecation in Symfony Using a Custom Rector Rule

Fixing Automatic Route Parameter Mapping Deprecation in Symfony Using a Custom Rector Rule

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:

  1. Identify the #[Route] Attribute:
    The rule scans the controller method for a #[Route] attribute.

  2. Extract the Placeholder from the Route Path:
    Using a regular expression, it identifies placeholders like {id} in the route path.

  3. 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! 🚀

comments powered by Disqus

Related Posts

Streamlining Deployment with Github Actions

Streamlining Deployment with Github Actions

Deploying my site used to be straightforward. I have a VPS on Linode that I set up using Ansible.

Read More
Faster queues with Symfony and Go: When PHP isn't Fast Enough

Faster queues with Symfony and Go: When PHP isn't Fast Enough

For most applications PHP is more than capable, and I’ve never run into a situation where it has been too slow, especially as I usually process long-running tasks in the background with Symfony Messenger.

Read More
Starting a Blog with Hugo and Adding a Contact Form

Starting a Blog with Hugo and Adding a Contact Form

Embarking on the journey of creating a personal blog or website often begins with the quest for simplicity and speed.

Read More