[Symfony] Add MergeMethodAnnotationToRouteAnnotationRector (#2020)

[Symfony] Add MergeMethodAnnotationToRouteAnnotationRector
This commit is contained in:
Tomáš Votruba 2019-09-24 20:56:47 +02:00 committed by GitHub
commit ce9f0b75de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 440 additions and 24 deletions

View File

@ -6,3 +6,4 @@ services:
# add Uuid type declarations
Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedParamTypeRector: ~
Rector\TypeDeclaration\Rector\ClassMethod\AddArrayReturnDocTypeRector: ~

View File

@ -4,3 +4,4 @@ services:
parse:
2:
- 'Symfony\Component\Yaml\Yaml::PARSE_KEYS_AS_STRINGS'
Rector\Symfony\Rector\ClassMethod\MergeMethodAnnotationToRouteAnnotationRector: ~

View File

@ -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, '#,#', ', ');
@ -40,7 +40,11 @@ abstract class AbstractTagValueNode implements AttributeAwareNodeInterface, PhpD
// 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;
}
/**

View File

@ -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;
}
}

View File

@ -5,7 +5,7 @@ namespace Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc;
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
@ -47,6 +47,11 @@ final class SymfonyRoutePhpDocTagValueNode extends AbstractTagValueNode
if ($originalContent !== null) {
$this->resolveOriginalContentSpacingAndOrder($originalContent);
// default value without key
if ($this->path && ! in_array('path', (array) $this->orderedVisibleItems, true)) {
$this->orderedVisibleItems[] = 'path';
}
}
}
@ -66,4 +71,13 @@ final class SymfonyRoutePhpDocTagValueNode extends AbstractTagValueNode
return $this->printContentItems($contentItems);
}
/**
* @param mixed[] $methods
*/
public function changeMethods(array $methods): void
{
$this->orderedVisibleItems[] = 'methods';
$this->methods = $methods;
}
}

View File

@ -11,7 +11,7 @@ use PhpParser\Node\Stmt\Class_;
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;
@ -256,7 +256,7 @@ PHP
}
$path = $this->resolvePathFromClassAndMethodNodes($presenterClass, $classMethod);
$symfonyRoutePhpDocTagValueNode = new SymfonyRoutePhpDocTagValueNode($path);
$symfonyRoutePhpDocTagValueNode = new SymfonyRouteTagValueNode($path);
$this->addSymfonyRouteShortTagNodeWithUse($symfonyRoutePhpDocTagValueNode, $classMethod);
}
@ -319,7 +319,7 @@ PHP
return false;
}
return (bool) $phpDocInfo->getByType(SymfonyRoutePhpDocTagValueNode::class);
return (bool) $phpDocInfo->getByType(SymfonyRouteTagValueNode::class);
}
private function resolvePathFromClassAndMethodNodes(Class_ $classNode, ClassMethod $classMethod): string
@ -338,23 +338,23 @@ PHP
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

View File

@ -22,7 +22,7 @@ final class SymfonyPhpDocParserExtension implements PhpDocParserExtensionInterfa
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

View File

@ -8,10 +8,12 @@ use PhpParser\Node\Stmt\Property;
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;
@ -25,9 +27,13 @@ final class SymfonyPhpDocTagParser extends AbstractPhpDocParser
// 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) {
@ -63,15 +69,15 @@ final class SymfonyPhpDocTagParser extends AbstractPhpDocParser
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(),
@ -79,6 +85,17 @@ final class SymfonyPhpDocTagParser extends AbstractPhpDocParser
);
}
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

View File

@ -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;
}
}

View File

@ -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)
{
}
}
?>

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace Rector\Symfony\Tests\Rector\ClassMethod\MergeMethodAnnotationToRouteAnnotationRector;
use Rector\Symfony\Rector\ClassMethod\MergeMethodAnnotationToRouteAnnotationRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class MergeMethodAnnotationToRouteAnnotationRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideDataForTest()
*/
public function test(string $file): void
{
$this->doTestFile($file);
}
/**
* @return string[]
*/
public function provideDataForTest(): iterable
{
yield [__DIR__ . '/Fixture/fixture.php.inc'];
}
protected function getRectorClass(): string
{
return MergeMethodAnnotationToRouteAnnotationRector::class;
}
}

View File

@ -28,6 +28,9 @@ final class AddArrayReturnDocTypeRectorTest extends AbstractRectorTestCase
yield [__DIR__ . '/Fixture/yield_strings.php.inc'];
yield [__DIR__ . '/Fixture/add_without_return_type_declaration.php.inc'];
yield [__DIR__ . '/Fixture/fix_incorrect_array.php.inc'];
yield [__DIR__ . '/Fixture/return_uuid.php.inc'];
// skip
yield [__DIR__ . '/Fixture/skip_shorten_class_name.php.inc'];
yield [__DIR__ . '/Fixture/skip_constructor.php.inc'];
yield [__DIR__ . '/Fixture/skip_inner_function_return.php.inc'];

View File

@ -0,0 +1,57 @@
<?php
namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddArrayReturnDocTypeRector\Fixture;
use Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddArrayReturnDocTypeRector\Source\EntityReturningUuid;
final class ReturnUuid
{
/**
* @var EntityReturningUuid[]
*/
private $amenityBuildings = [];
/**
* @return int[]
*/
public function getBuildingIds(): array
{
$buildingIds = [];
foreach ($this->amenityBuildings as $amenityBuilding) {
$buildingIds[] = $amenityBuilding->getId();
}
return $buildingIds;
}
}
?>
-----
<?php
namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddArrayReturnDocTypeRector\Fixture;
use Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddArrayReturnDocTypeRector\Source\EntityReturningUuid;
final class ReturnUuid
{
/**
* @var EntityReturningUuid[]
*/
private $amenityBuildings = [];
/**
* @return \Ramsey\Uuid\UuidInterface[]
*/
public function getBuildingIds(): array
{
$buildingIds = [];
foreach ($this->amenityBuildings as $amenityBuilding) {
$buildingIds[] = $amenityBuilding->getId();
}
return $buildingIds;
}
}
?>

View File

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddArrayReturnDocTypeRector\Source;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
final class EntityReturningUuid
{
public function getId(): UuidInterface
{
return Uuid::uuid4();
}
}

View File

@ -11,7 +11,7 @@ use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\SpacelessPhpDocTagNode;
use Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc\SymfonyRoutePhpDocTagValueNode;
use Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc\SymfonyRouteTagValueNode;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\PHPStan\Type\FullyQualifiedObjectType;
@ -141,13 +141,13 @@ PHP
{
$symfonyRoutePhpDocTagNode = $routeValueObject->getSymfonyRoutePhpDocTagNode();
$symfonyRoutePhpDocNode = new SpacelessPhpDocTagNode(
SymfonyRoutePhpDocTagValueNode::SHORT_NAME,
SymfonyRouteTagValueNode::SHORT_NAME,
$symfonyRoutePhpDocTagNode
);
$this->docBlockManipulator->addTag($classMethod, $symfonyRoutePhpDocNode);
$this->addUseType(new FullyQualifiedObjectType(SymfonyRoutePhpDocTagValueNode::CLASS_NAME), $classMethod);
$this->addUseType(new FullyQualifiedObjectType(SymfonyRouteTagValueNode::CLASS_NAME), $classMethod);
}
/**

View File

@ -3,7 +3,7 @@
namespace Rector\ZendToSymfony\ValueObject;
use Nette\Utils\Strings;
use Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc\SymfonyRoutePhpDocTagValueNode;
use Rector\NetteToSymfony\PhpDocParser\Ast\PhpDoc\SymfonyRouteTagValueNode;
use Rector\Util\RectorStrings;
final class RouteValueObject
@ -51,9 +51,9 @@ final class RouteValueObject
return $this->params;
}
public function getSymfonyRoutePhpDocTagNode(): SymfonyRoutePhpDocTagValueNode
public function getSymfonyRoutePhpDocTagNode(): SymfonyRouteTagValueNode
{
return new SymfonyRoutePhpDocTagValueNode($this->getPath());
return new SymfonyRouteTagValueNode($this->getPath());
}
private function getPath(): string

View File

@ -0,0 +1,16 @@
<?php declare(strict_types=1);
namespace Sensio\Bundle\FrameworkExtraBundle\Configuration;
abstract class ConfigurationAnnotation
{
public function __construct(array $values)
{
foreach ($values as $k => $v) {
if (!method_exists($this, $name = 'set'.$k)) {
throw new \RuntimeException(sprintf('Unknown key "%s" for annotation "@%s".', $k, \get_class($this)));
}
$this->$name($v);
}
}
}

View File

@ -0,0 +1,69 @@
<?php declare(strict_types=1);
namespace Sensio\Bundle\FrameworkExtraBundle\Configuration;
if (class_exists('Sensio\Bundle\FrameworkExtraBundle\Configuration\Method')) {
return;
}
/**
* @Annotation
*/
class Method extends ConfigurationAnnotation
{
/**
* An array of restricted HTTP methods.
*
* @var array
*/
private $methods = [];
/**
* Returns the array of HTTP methods.
*
* @return array
*/
public function getMethods()
{
return $this->methods;
}
/**
* Sets the HTTP methods.
*
* @param array|string $methods An HTTP method or an array of HTTP methods
*/
public function setMethods($methods)
{
$this->methods = \is_array($methods) ? $methods : [$methods];
}
/**
* Sets the HTTP methods.
*
* @param array|string $methods An HTTP method or an array of HTTP methods
*/
public function setValue($methods)
{
$this->setMethods($methods);
}
/**
* Returns the annotation alias name.
*
* @return string
*
* @see ConfigurationInterface
*/
public function getAliasName()
{
return 'method';
}
/**
* Only one method directive is allowed.
*
* @return bool
*
* @see ConfigurationInterface
*/
public function allowArray()
{
return false;
}
}