Skip to content

JsonResponse rule for Symfony refactor #783

Open
@shakaran

Description

@shakaran
+use Symfony\Component\HttpFoundation\Response;
 
 class SomeController extends AbstractController
 {
-public function createTemplate(): JsonResponse
+public function createTemplate(): Response
{
-    return new JsonResponse(['data' => $response], JsonResponse::HTTP_OK);
+    return $this->json(['data' => $response], Response::HTTP_OK);
}
 }

I would like know if it is possible include a symfony rule that renames all constants JsonResponse::HTTP_OK or JsonResponse::*
to Response::HTTP_OK or Response::*

This solves warnings in IDE like:
"Constant from class 'Symfony\Component\HttpFoundation\Response' referenced through child."

And put the good practice of use $this->json() method instead return the object new JsonResponse()

Also the return typing changing

:JsonResponse

to

:Response

src/Rector/Response/JsonResponseToControllerJsonRector.php

<?php

declare(strict_types=1);

namespace Utils\Rector\Rector;

use PhpParser\Node;
use Rector\Rector\AbstractRector;

use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Name;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Name\FullyQualified;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;

use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;


/**
 * @see \Rector\Tests\TypeDeclaration\Rector\JsonResponseToControllerJsonRector\JsonResponseToControllerJsonRectorTest
 */
final class JsonResponseToControllerJsonRector extends AbstractRector
{
    /**
     * Adds a 'use' import for the given class if it does not already exist.
     */
    private function addUseImportToFile(Node $node, string $className): void
    {
        $file = $node->getAttribute('file');
        if ($file && method_exists($this, 'addUseType')) {
            // For compatibility with Rector's addUseType if available
            $this->addUseType($node, $className);
            return;
        }
    }

    /**
     * @return array<class-string<Node>>
     */
    public function getNodeTypes(): array
    {
        return [ClassMethod::class, ClassConstFetch::class, New_::class];
    }

    public function getRuleDefinition(): RuleDefinition
    {
        return new RuleDefinition('Replace JsonResponse with Response and use $this->json()', [
            new CodeSample(
                badCode: <<<'CODE'
use Symfony\Component\HttpFoundation\JsonResponse;

class SomeController extends AbstractController
{
    public function createTemplate(): JsonResponse
    {
        return new JsonResponse(['data' => $response], JsonResponse::HTTP_OK);
    }
}
CODE
                ,
                goodCode: <<<'CODE'
use Symfony\Component\HttpFoundation\Response;

class SomeController extends AbstractController
{
    public function createTemplate(): Response
    {
        return $this->json(['data' => $response], Response::HTTP_OK);
    }
}
CODE
            )
        ]);
    }

    public function refactor(Node $node): ?Node
    {
        // Replace return new JsonResponse(...) with $this->json(...)
        if ($node instanceof New_ && $this->getName($node->class) === JsonResponse::class) {
            // Corner case: new JsonResponse() with no arguments
            if (count($node->args) === 0) {
              // Ensure 'use Symfony\Component\HttpFoundation\Response;' is present
              $this->addUseImportToFile($node, Response::class);
              return new MethodCall(
                  new Node\Expr\Variable('this'),
                  'json',
                  [
                new Node\Arg(new Node\Expr\Array_([])),
                new Node\Arg(new ClassConstFetch(new Name('Response'), 'HTTP_OK'))
                  ]
              );
            }
            return new MethodCall(new Node\Expr\Variable('this'), 'json', $node->args);
        }

        // Replace JsonResponse::HTTP_* with Response::HTTP_*
        if ($node instanceof ClassConstFetch && $this->getName($node->class) === JsonResponse::class) {
            // Ensure 'use Symfony\Component\HttpFoundation\Response;' is present
            $this->addUseImportToFile($node, Response::class);
            $node->class = new Name('Response');
            return $node;
        }

        // Change return type from JsonResponse to Response
        if ($node instanceof ClassMethod) {
            if ($node->returnType instanceof Name && $this->getName($node->returnType) === JsonResponse::class) {
                $node->returnType = new FullyQualified(Response::class);
                return $node;
            }
        }

        return null;
    }
}

Using in rector.php

use App\Rector\Response\JsonResponseToControllerJsonRector;

return static function (Rector\Config\RectorConfig $rectorConfig): void {
    $rectorConfig->paths([__DIR__ . '/src']);

    $rectorConfig->rule(JsonResponseToControllerJsonRector::class);

    $rectorConfig->importNames();
};

Test before:

use Symfony\Component\HttpFoundation\JsonResponse;

public function index(): JsonResponse
{
    return new JsonResponse(['message' => 'ok'], JsonResponse::HTTP_OK);
}

Test after:

use Symfony\Component\HttpFoundation\Response;

public function index(): Response
{
    return $this->json(['message' => 'ok'], Response::HTTP_OK);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions