Skip to content

Commit

Permalink
[Symfony] Add MergeMethodAnnotationToRouteAnnotationRector
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba committed Sep 24, 2019
1 parent 4fbca58 commit 3756e25
Show file tree
Hide file tree
Showing 15 changed files with 367 additions and 26 deletions.
1 change: 1 addition & 0 deletions config/set/symfony/symfony34.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ services:
parse:
2:
- 'Symfony\Component\Yaml\Yaml::PARSE_KEYS_AS_STRINGS'
Rector\Symfony\Rector\ClassMethod\MergeMethodAnnotationToRouteAnnotationRector: ~
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ abstract class AbstractTagValueNode implements AttributeAwareNodeInterface, PhpD
/**
* @param mixed[] $item
*/
protected function printArrayItem(array $item, string $key): string
protected function printArrayItem(array $item, ?string $key = null): string
{
$json = Json::encode($item);
$json = Strings::replace($json, '#,#', ', ');
Expand All @@ -40,7 +40,11 @@ protected function printArrayItem(array $item, string $key): string
// cleanup json encoded extra slashes
$json = Strings::replace($json, '#\\\\\\\\#', '\\');

return sprintf('%s=%s', $key, $json);
if ($key) {
return sprintf('%s=%s', $key, $json);
}

return $json;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php declare(strict_types=1);

namespace Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc;

use Rector\BetterPhpDocParser\PhpDocParser\Ast\PhpDoc\AbstractTagValueNode;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;

final class SymfonyMethodTagValueNode extends AbstractTagValueNode
{
/**
* @var string
*/
public const SHORT_NAME = '@Method';

/**
* @var string
*/
public const CLASS_NAME = Method::class;

/**
* @var string[]
*/
private $methods = [];

/**
* @param string[] $methods
*/
public function __construct(array $methods = [])
{
$this->methods = $methods;
}

public function __toString(): string
{
return '(' . $this->printArrayItem($this->methods) . ')';
}

/**
* @return string[]
*/
public function getMethods(): array
{
return $this->methods;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Rector\BetterPhpDocParser\PhpDocParser\Ast\PhpDoc\AbstractTagValueNode;
use Symfony\Component\Routing\Annotation\Route;

final class SymfonyRoutePhpDocTagValueNode extends AbstractTagValueNode
final class SymfonyRouteTagValueNode extends AbstractTagValueNode
{
/**
* @var string
Expand Down Expand Up @@ -48,6 +48,11 @@ public function __construct(
if ($originalContent !== null) {
$this->resolveOriginalContentSpacingAndOrder($originalContent);
}

// default value without key
if ($this->path && ! in_array('path', (array) $this->orderedVisibleItems, true)) {
$this->orderedVisibleItems[] = 'path';
}
}

public function __toString(): string
Expand All @@ -66,4 +71,13 @@ public function __toString(): string

return $this->printContentItems($contentItems);
}

/**
* @param mixed[] $methods
*/
public function changeMethods(array $methods): void
{
$this->orderedVisibleItems[] = 'methods';
$this->methods = $methods;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Type\ObjectType;
use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\SpacelessPhpDocTagNode;
use Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc\SymfonyRoutePhpDocTagValueNode;
use Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc\SymfonyRouteTagValueNode;
use Rector\NetteToSymfony\Route\RouteInfo;
use Rector\NetteToSymfony\Route\RouteInfoFactory;
use Rector\NodeContainer\ParsedNodesByType;
Expand Down Expand Up @@ -256,7 +256,7 @@ private function completeImplicitRoutes(): void
}

$path = $this->resolvePathFromClassAndMethodNodes($presenterClass, $classMethod);
$symfonyRoutePhpDocTagValueNode = new SymfonyRoutePhpDocTagValueNode($path);
$symfonyRoutePhpDocTagValueNode = new SymfonyRouteTagValueNode($path);

$this->addSymfonyRouteShortTagNodeWithUse($symfonyRoutePhpDocTagValueNode, $classMethod);
}
Expand Down Expand Up @@ -319,7 +319,7 @@ private function shouldSkipClassStmt(Node $node): bool
return false;
}

return (bool) $phpDocInfo->getByType(SymfonyRoutePhpDocTagValueNode::class);
return (bool) $phpDocInfo->getByType(SymfonyRouteTagValueNode::class);
}

private function resolvePathFromClassAndMethodNodes(Class_ $classNode, ClassMethod $classMethod): string
Expand All @@ -338,23 +338,23 @@ private function resolvePathFromClassAndMethodNodes(Class_ $classNode, ClassMeth
return $presenterPart . '/' . $actionPart;
}

private function createSymfonyRoutePhpDocTagValueNode(RouteInfo $routeInfo): SymfonyRoutePhpDocTagValueNode
private function createSymfonyRoutePhpDocTagValueNode(RouteInfo $routeInfo): SymfonyRouteTagValueNode
{
return new SymfonyRoutePhpDocTagValueNode($routeInfo->getPath(), null, $routeInfo->getHttpMethods());
return new SymfonyRouteTagValueNode($routeInfo->getPath(), null, $routeInfo->getHttpMethods());
}

private function addSymfonyRouteShortTagNodeWithUse(
SymfonyRoutePhpDocTagValueNode $symfonyRoutePhpDocTagValueNode,
SymfonyRouteTagValueNode $symfonyRouteTagValueNode,
ClassMethod $classMethod
): void {
$symfonyRoutePhpDocTagNode = new SpacelessPhpDocTagNode(
SymfonyRoutePhpDocTagValueNode::SHORT_NAME,
$symfonyRoutePhpDocTagValueNode
SymfonyRouteTagValueNode::SHORT_NAME,
$symfonyRouteTagValueNode
);

$this->docBlockManipulator->addTag($classMethod, $symfonyRoutePhpDocTagNode);

$symfonyRouteUseObjectType = new FullyQualifiedObjectType(SymfonyRoutePhpDocTagValueNode::CLASS_NAME);
$symfonyRouteUseObjectType = new FullyQualifiedObjectType(SymfonyRouteTagValueNode::CLASS_NAME);
$this->addUseType($symfonyRouteUseObjectType, $classMethod);

// remove
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function __construct(SymfonyPhpDocTagParser $symfonyPhpDocTagParser)

public function matchTag(string $tag): bool
{
return (bool) Strings::match($tag, '#^@(Route|((Assert|Serializer)\\\\[\w]+))$#');
return (bool) Strings::match($tag, '#^@(Route|Method|((Assert|Serializer)\\\\[\w]+))$#');
}

public function parse(TokenIterator $tokenIterator, string $tag): ?PhpDocTagValueNode
Expand Down
27 changes: 22 additions & 5 deletions packages/Symfony/src/PhpDocParser/SymfonyPhpDocTagParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use Rector\BetterPhpDocParser\PhpDocParser\AbstractPhpDocParser;
use Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc\SymfonyRoutePhpDocTagValueNode;
use Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc\SymfonyMethodTagValueNode;
use Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc\SymfonyRouteTagValueNode;
use Rector\Symfony\PhpDocParser\Ast\PhpDoc\AssertChoiceTagValueNode;
use Rector\Symfony\PhpDocParser\Ast\PhpDoc\AssertTypeTagValueNode;
use Rector\Symfony\PhpDocParser\Ast\PhpDoc\SerializerTypeTagValueNode;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints\Choice;
use Symfony\Component\Validator\Constraints\Type as ValidatorType;
Expand All @@ -25,9 +27,13 @@ public function parse(TokenIterator $tokenIterator, string $tag): ?PhpDocTagValu
// this is needed to append tokens to the end of annotation, even if not used
$annotationContent = $this->resolveAnnotationContent($tokenIterator);
if ($currentPhpNode instanceof ClassMethod) {
if ($tag === SymfonyRoutePhpDocTagValueNode::SHORT_NAME) {
if ($tag === SymfonyRouteTagValueNode::SHORT_NAME) {
return $this->createSymfonyRouteTagValueNode($currentPhpNode, $annotationContent);
}

if ($tag === SymfonyMethodTagValueNode::SHORT_NAME) {
return $this->createSymfonyMethodTagValueNode($currentPhpNode);
}
}

if ($currentPhpNode instanceof Property) {
Expand Down Expand Up @@ -63,22 +69,33 @@ private function createAssertChoiceTagValueNode(
private function createSymfonyRouteTagValueNode(
ClassMethod $classMethod,
string $annotationContent
): SymfonyRoutePhpDocTagValueNode {
): SymfonyRouteTagValueNode {
/** @var Route $routeAnnotation */
$routeAnnotation = $this->nodeAnnotationReader->readMethodAnnotation(
$classMethod,
SymfonyRoutePhpDocTagValueNode::CLASS_NAME
SymfonyRouteTagValueNode::CLASS_NAME
);

// @todo possibly extends with all Symfony Route attributes
return new SymfonyRoutePhpDocTagValueNode(
return new SymfonyRouteTagValueNode(
$routeAnnotation->getPath(),
$routeAnnotation->getName(),
$routeAnnotation->getMethods(),
$annotationContent
);
}

private function createSymfonyMethodTagValueNode(ClassMethod $classMethod): SymfonyMethodTagValueNode
{
/** @var Method $methodAnnotation */
$methodAnnotation = $this->nodeAnnotationReader->readMethodAnnotation(
$classMethod,
SymfonyMethodTagValueNode::CLASS_NAME
);

return new SymfonyMethodTagValueNode($methodAnnotation->getMethods());
}

private function createSerializerTypeTagValueNode(
Property $property,
string $annotationContent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php declare(strict_types=1);

namespace Rector\Symfony\Rector\ClassMethod;

use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc\SymfonyMethodTagValueNode;
use Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc\SymfonyRouteTagValueNode;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;

/**
* @see https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/routing.html#method-annotation
* @see https://stackoverflow.com/questions/51171934/how-to-fix-symfony-3-4-route-and-method-deprecation
*
* @see \Rector\Symfony\Tests\Rector\ClassMethod\MergeMethodAnnotationToRouteAnnotationRector\MergeMethodAnnotationToRouteAnnotationRectorTest
*/
final class MergeMethodAnnotationToRouteAnnotationRector extends AbstractRector
{
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Merge removed @Method annotation to @Route one', [
new CodeSample(
<<<'PHP'
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\Routing\Annotation\Route;
class DefaultController extends Controller
{
/**
* @Route("/show/{id}")
* @Method({"GET", "HEAD"})
*/
public function show($id)
{
}
}
PHP
,
<<<'PHP'
use Symfony\Component\Routing\Annotation\Route;
class DefaultController extends Controller
{
/**
* @Route("/show/{id}", methods={"GET","HEAD"})
*/
public function show($id)
{
}
}
PHP
),
]);
}

/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}

/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
$classNode = $node->getAttribute(AttributeKey::CLASS_NODE);
if ($classNode === null) {
return null;
}

if (! $this->isObjectType($classNode, '*Controller')) {
return null;
}

if (! $node->isPublic()) {
return null;
}

$phpDocInfo = $this->getPhpDocInfo($node);
if ($phpDocInfo === null) {
return null;
}

$symfonyMethodPhpDocTagValueNode = $phpDocInfo->getByType(SymfonyMethodTagValueNode::class);
if ($symfonyMethodPhpDocTagValueNode === null) {
return null;
}

$methods = $symfonyMethodPhpDocTagValueNode->getMethods();

/** @var SymfonyRouteTagValueNode $symfonyRoutePhpDocTagValueNode */
$symfonyRoutePhpDocTagValueNode = $phpDocInfo->getByType(SymfonyRouteTagValueNode::class);
$symfonyRoutePhpDocTagValueNode->changeMethods($methods);

$phpDocInfo->removeTagValueNodeFromNode($symfonyMethodPhpDocTagValueNode);

$this->docBlockManipulator->updateNodeWithPhpDocInfo($node, $phpDocInfo);

return $node;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Rector\Symfony\Tests\Rector\ClassMethod\MergeMethodAnnotationToRouteAnnotationRector\Fixture;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController
{
/**
* @Route("/show/{id}")
* @Method({"GET", "HEAD"})
*/
public function show($id)
{
}
}

?>
-----
<?php

namespace Rector\Symfony\Tests\Rector\ClassMethod\MergeMethodAnnotationToRouteAnnotationRector\Fixture;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController
{
/**
* @Route(path="/show/{id}", methods={"GET", "HEAD"})
*/
public function show($id)
{
}
}

?>
Loading

0 comments on commit 3756e25

Please sign in to comment.