[PHP 8.0] Refactor attributes from magic interface to explicit list (#5926)

This commit is contained in:
Tomas Votruba 2021-03-21 00:16:21 +01:00 committed by GitHub
parent 44375f6637
commit 68026636bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 214 additions and 617 deletions

View File

@ -27,7 +27,6 @@ use Rector\Core\Configuration\CurrentNodeProvider;
use Rector\Core\Exception\NotImplementedYetException;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Util\StaticInstanceOf;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
use Rector\StaticTypeMapper\StaticTypeMapper;
/**
@ -493,7 +492,6 @@ final class PhpDocInfo
$desiredTypes = array_merge([
PhpDocTagValueNode::class,
PhpDocTagNode::class,
PhpAttributableTagNodeInterface::class,
], NodeTypes::TYPE_AWARE_NODES);
if (StaticInstanceOf::isOneOf($type, $desiredTypes)) {

View File

@ -7,13 +7,11 @@ namespace Rector\BetterPhpDocParser\ValueObject\PhpDoc;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
/**
* Use by Symfony to autowire dependencies outside constructor,
* @see https://symfony.com/doc/current/service_container/autowiring.html#autowiring-other-methods-e-g-setters-and-public-typed-properties
*/
final class SymfonyRequiredTagNode extends PhpDocTagNode implements PhpAttributableTagNodeInterface
final class SymfonyRequiredTagNode extends PhpDocTagNode
{
/**
* @var string
@ -34,17 +32,4 @@ final class SymfonyRequiredTagNode extends PhpDocTagNode implements PhpAttributa
{
return self::NAME;
}
public function getAttributeClassName(): string
{
return 'Symfony\Contracts\Service\Attribute\Required';
}
/**
* @return mixed[]
*/
public function getAttributableItems(): array
{
return [];
}
}

View File

@ -73,6 +73,14 @@ abstract class AbstractTagValueNode implements PhpDocTagValueNode
return $this->items;
}
/**
* @return mixed[]
*/
public function getItemsWithoutDefaults(): array
{
return $this->filterOutMissingItems($this->items);
}
/**
* @param mixed $value
*/

View File

@ -10,11 +10,4 @@ use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\AbstractTagValueNode;
abstract class AbstractDoctrineTagValueNode extends AbstractTagValueNode implements DoctrineTagNodeInterface, ShortNameAwareTagInterface
{
/**
* @return mixed[]
*/
public function getAttributableItems(): array
{
return $this->filterOutMissingItems($this->items);
}
}

View File

@ -5,10 +5,8 @@ declare(strict_types=1);
namespace Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Doctrine\Class_;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Doctrine\AbstractDoctrineTagValueNode;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
final class EntityTagValueNode extends AbstractDoctrineTagValueNode implements PhpAttributableTagNodeInterface
final class EntityTagValueNode extends AbstractDoctrineTagValueNode
{
/**
* @var string
@ -34,17 +32,4 @@ final class EntityTagValueNode extends AbstractDoctrineTagValueNode implements P
{
return '@ORM\Entity';
}
/**
* @return mixed[]
*/
public function getAttributableItems(): array
{
return $this->filterOutMissingItems($this->items);
}
public function getAttributeClassName(): string
{
return PhpAttributeGroupFactory::TO_BE_ANNOUNCED;
}
}

View File

@ -5,10 +5,8 @@ declare(strict_types=1);
namespace Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Doctrine\Property_;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Doctrine\AbstractDoctrineTagValueNode;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
final class ColumnTagValueNode extends AbstractDoctrineTagValueNode implements PhpAttributableTagNodeInterface
final class ColumnTagValueNode extends AbstractDoctrineTagValueNode
{
public function changeType(string $type): void
{
@ -37,9 +35,4 @@ final class ColumnTagValueNode extends AbstractDoctrineTagValueNode implements P
{
return $this->items['options'] ?? [];
}
public function getAttributeClassName(): string
{
return PhpAttributeGroupFactory::TO_BE_ANNOUNCED;
}
}

View File

@ -6,13 +6,11 @@ namespace Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Doctrine\Property_;
use Rector\BetterPhpDocParser\Contract\PhpDocNode\SilentKeyNodeInterface;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Doctrine\AbstractDoctrineTagValueNode;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
/**
* @see \Rector\Tests\BetterPhpDocParser\PhpDocParser\TagValueNodeReprint\TagValueNodeReprintTest
*/
final class GeneratedValueTagValueNode extends AbstractDoctrineTagValueNode implements PhpAttributableTagNodeInterface, SilentKeyNodeInterface
final class GeneratedValueTagValueNode extends AbstractDoctrineTagValueNode implements SilentKeyNodeInterface
{
public function getShortName(): string
{
@ -23,9 +21,4 @@ final class GeneratedValueTagValueNode extends AbstractDoctrineTagValueNode impl
{
return 'strategy';
}
public function getAttributeClassName(): string
{
return PhpAttributeGroupFactory::TO_BE_ANNOUNCED;
}
}

View File

@ -5,18 +5,11 @@ declare(strict_types=1);
namespace Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Doctrine\Property_;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Doctrine\AbstractDoctrineTagValueNode;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
final class IdTagValueNode extends AbstractDoctrineTagValueNode implements PhpAttributableTagNodeInterface
final class IdTagValueNode extends AbstractDoctrineTagValueNode
{
public function getShortName(): string
{
return '@ORM\Id';
}
public function getAttributeClassName(): string
{
return PhpAttributeGroupFactory::TO_BE_ANNOUNCED;
}
}

View File

@ -8,10 +8,8 @@ use Rector\BetterPhpDocParser\Contract\PhpDocNode\TagAwareNodeInterface;
use Rector\BetterPhpDocParser\Printer\ArrayPartPhpDocTagPrinter;
use Rector\BetterPhpDocParser\Printer\TagValueNodePrinter;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Doctrine\AbstractDoctrineTagValueNode;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
final class JoinColumnTagValueNode extends AbstractDoctrineTagValueNode implements TagAwareNodeInterface, PhpAttributableTagNodeInterface
final class JoinColumnTagValueNode extends AbstractDoctrineTagValueNode implements TagAwareNodeInterface
{
/**
* @var string
@ -64,9 +62,4 @@ final class JoinColumnTagValueNode extends AbstractDoctrineTagValueNode implemen
{
$this->shortName = $shortName;
}
public function getAttributeClassName(): string
{
return PhpAttributeGroupFactory::TO_BE_ANNOUNCED;
}
}

View File

@ -9,11 +9,8 @@ use Rector\BetterPhpDocParser\Printer\TagValueNodePrinter;
use Rector\BetterPhpDocParser\ValueObject\AroundSpaces;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Doctrine\AbstractDoctrineTagValueNode;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\PhpAttribute\Contract\ManyPhpAttributableTagNodeInterface;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
final class JoinTableTagValueNode extends AbstractDoctrineTagValueNode implements PhpAttributableTagNodeInterface, ManyPhpAttributableTagNodeInterface
final class JoinTableTagValueNode extends AbstractDoctrineTagValueNode
{
/**
* @var string
@ -96,47 +93,6 @@ final class JoinTableTagValueNode extends AbstractDoctrineTagValueNode implement
return '@ORM\JoinTable';
}
/**
* @return mixed[]
*/
public function getAttributableItems(): array
{
$items = [];
if ($this->name !== null) {
$items['name'] = $this->name;
}
if ($this->schema !== null) {
$items['schema'] = $this->schema;
}
return $items;
}
/**
* @return array<string, mixed[]>
*/
public function provide(): array
{
$items = [];
foreach ($this->joinColumns as $joinColumn) {
$items[$joinColumn->getShortName()] = $joinColumn->getAttributableItems();
}
foreach ($this->inverseJoinColumns as $inverseJoinColumn) {
$items['@ORM\InverseJoinColumn'] = $inverseJoinColumn->getAttributableItems();
}
return $items;
}
public function getAttributeClassName(): string
{
return PhpAttributeGroupFactory::TO_BE_ANNOUNCED;
}
/**
* @return string[]
*/

View File

@ -10,10 +10,8 @@ use Rector\BetterPhpDocParser\Contract\Doctrine\ToManyTagNodeInterface;
use Rector\BetterPhpDocParser\Printer\ArrayPartPhpDocTagPrinter;
use Rector\BetterPhpDocParser\Printer\TagValueNodePrinter;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Doctrine\AbstractDoctrineTagValueNode;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
final class ManyToManyTagValueNode extends AbstractDoctrineTagValueNode implements ToManyTagNodeInterface, MappedByNodeInterface, InversedByNodeInterface, PhpAttributableTagNodeInterface
final class ManyToManyTagValueNode extends AbstractDoctrineTagValueNode implements ToManyTagNodeInterface, MappedByNodeInterface, InversedByNodeInterface
{
/**
* @var string
@ -76,9 +74,4 @@ final class ManyToManyTagValueNode extends AbstractDoctrineTagValueNode implemen
{
return '@ORM\ManyToMany';
}
public function getAttributeClassName(): string
{
return PhpAttributeGroupFactory::TO_BE_ANNOUNCED;
}
}

View File

@ -8,12 +8,11 @@ use Rector\BetterPhpDocParser\Contract\PhpDocNode\ClassNameAwareTagInterface;
use Rector\BetterPhpDocParser\Contract\PhpDocNode\ShortNameAwareTagInterface;
use Rector\BetterPhpDocParser\Contract\PhpDocNode\SilentKeyNodeInterface;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\AbstractTagValueNode;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
/**
* @see \Rector\Tests\BetterPhpDocParser\PhpDocParser\TagValueNodeReprint\TagValueNodeReprintTest
*/
final class SymfonyRouteTagValueNode extends AbstractTagValueNode implements ShortNameAwareTagInterface, SilentKeyNodeInterface, PhpAttributableTagNodeInterface, ClassNameAwareTagInterface
final class SymfonyRouteTagValueNode extends AbstractTagValueNode implements ShortNameAwareTagInterface, SilentKeyNodeInterface, ClassNameAwareTagInterface
{
/**
* @var string
@ -59,19 +58,6 @@ final class SymfonyRouteTagValueNode extends AbstractTagValueNode implements Sho
$this->tagValueNodeConfiguration->mimic($abstractTagValueNode->tagValueNodeConfiguration);
}
/**
* @return mixed[]
*/
public function getAttributableItems(): array
{
return $this->filterOutMissingItems($this->items);
}
public function getAttributeClassName(): string
{
return self::CLASS_NAME;
}
public function getClassName(): string
{
return self::CLASS_NAME;

View File

@ -7,12 +7,11 @@ namespace Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Symfony\Validator\Con
use Rector\BetterPhpDocParser\Contract\PhpDocNode\ShortNameAwareTagInterface;
use Rector\BetterPhpDocParser\Contract\PhpDocNode\SilentKeyNodeInterface;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\AbstractTagValueNode;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
/**
* @see \Rector\Tests\BetterPhpDocParser\PhpDocParser\TagValueNodeReprint\TagValueNodeReprintTest
*/
final class AssertEmailTagValueNode extends AbstractTagValueNode implements ShortNameAwareTagInterface, PhpAttributableTagNodeInterface, SilentKeyNodeInterface
final class AssertEmailTagValueNode extends AbstractTagValueNode implements ShortNameAwareTagInterface, SilentKeyNodeInterface
{
public function getShortName(): string
{
@ -23,17 +22,4 @@ final class AssertEmailTagValueNode extends AbstractTagValueNode implements Shor
{
return 'choices';
}
/**
* @return mixed[]
*/
public function getAttributableItems(): array
{
return $this->filterOutMissingItems($this->items);
}
public function getAttributeClassName(): string
{
return 'Symfony\Component\Validator\Constraints\Email';
}
}

View File

@ -6,25 +6,11 @@ namespace Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Symfony\Validator\Con
use Rector\BetterPhpDocParser\Contract\PhpDocNode\ShortNameAwareTagInterface;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\AbstractTagValueNode;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
final class AssertRangeTagValueNode extends AbstractTagValueNode implements ShortNameAwareTagInterface, PhpAttributableTagNodeInterface
final class AssertRangeTagValueNode extends AbstractTagValueNode implements ShortNameAwareTagInterface
{
public function getShortName(): string
{
return '@Assert\Range';
}
/**
* @return mixed[]
*/
public function getAttributableItems(): array
{
return $this->filterOutMissingItems($this->items);
}
public function getAttributeClassName(): string
{
return 'Symfony\Component\Validator\Constraints\Range';
}
}

View File

@ -1,111 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\PhpAttribute;
use PhpParser\Node;
use PhpParser\Node\Attribute;
use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Property;
use PHPStan\Reflection\ReflectionProvider;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
use Rector\Testing\PHPUnit\StaticPHPUnitEnvironment;
final class AnnotationToAttributeConverter
{
/**
* @var PhpAttributeGroupFactory
*/
private $phpAttributeGroupFactory;
/**
* @var PhpDocInfoFactory
*/
private $phpDocInfoFactory;
/**
* @var PhpDocTagRemover
*/
private $phpDocTagRemover;
/**
* @var ReflectionProvider
*/
private $reflectionProvider;
public function __construct(
PhpAttributeGroupFactory $phpAttributeGroupFactory,
PhpDocInfoFactory $phpDocInfoFactory,
PhpDocTagRemover $phpDocTagRemover,
ReflectionProvider $reflectionProvider
) {
$this->phpAttributeGroupFactory = $phpAttributeGroupFactory;
$this->phpDocInfoFactory = $phpDocInfoFactory;
$this->phpDocTagRemover = $phpDocTagRemover;
$this->reflectionProvider = $reflectionProvider;
}
/**
* @param Class_|Property|ClassMethod|Function_|Closure|ArrowFunction $node
*/
public function convertNode(Node $node): ?Node
{
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
// 0. has 0 nodes, nothing to change
/** @var PhpAttributableTagNodeInterface[] $phpAttributableTagNodes */
$phpAttributableTagNodes = $phpDocInfo->findAllByType(PhpAttributableTagNodeInterface::class);
if ($phpAttributableTagNodes === []) {
return null;
}
$hasNewAttrGroups = false;
// 1. keep only those, whom's attribute class exists
$phpAttributableTagNodes = $this->filterOnlyExistingAttributes($phpAttributableTagNodes);
if ($phpAttributableTagNodes !== []) {
$hasNewAttrGroups = true;
}
// 2. remove tags
foreach ($phpAttributableTagNodes as $phpAttributableTagNode) {
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $phpAttributableTagNode);
}
// 3. convert annotations to attributes
$newAttrGroups = $this->phpAttributeGroupFactory->create($phpAttributableTagNodes);
$node->attrGroups = array_merge($node->attrGroups, $newAttrGroups);
if ($hasNewAttrGroups) {
return $node;
}
return null;
}
/**
* @param PhpAttributableTagNodeInterface[] $phpAttributableTagNodes
* @return PhpAttributableTagNodeInterface[]
*/
private function filterOnlyExistingAttributes(array $phpAttributableTagNodes): array
{
if (StaticPHPUnitEnvironment::isPHPUnitRun()) {
return $phpAttributableTagNodes;
}
return array_filter(
$phpAttributableTagNodes,
function (PhpAttributableTagNodeInterface $phpAttributableTagNode): bool {
return $this->reflectionProvider->hasClass($phpAttributableTagNode->getAttributeClassName());
}
);
}
}

View File

@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\PhpAttribute\Contract;
interface ManyPhpAttributableTagNodeInterface
{
/**
* @return array<string, mixed[]>
*/
public function provide(): array;
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\PhpAttribute\Contract;
use PHPStan\PhpDocParser\Ast\Node;
interface PhpAttributableTagNodeInterface extends Node
{
public function getShortName(): string;
public function getAttributeClassName(): string;
/**
* @return mixed[]
*/
public function getAttributableItems(): array;
}

View File

@ -9,66 +9,25 @@ use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use Rector\PhpAttribute\Contract\ManyPhpAttributableTagNodeInterface;
use Rector\PhpAttribute\Contract\PhpAttributableTagNodeInterface;
use PHPStan\PhpDocParser\Ast\Node;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\AbstractTagValueNode;
use Rector\Php80\ValueObject\AnnotationToAttribute;
final class PhpAttributeGroupFactory
{
/**
* A dummy placeholder for annotation, that we know will be converted to attributes,
* but don't have specific attribute class yet.
*
* @var string
*/
public const TO_BE_ANNOUNCED = 'TBA';
/**
* @param PhpAttributableTagNodeInterface[] $phpAttributableTagNodes
* @return AttributeGroup[]
*/
public function create(array $phpAttributableTagNodes): array
public function create(Node $node, AnnotationToAttribute $annotationToAttribute): AttributeGroup
{
$attributeGroups = [];
foreach ($phpAttributableTagNodes as $phpAttributableTagNode) {
$currentAttributeGroups = $this->printPhpAttributableTagNode($phpAttributableTagNode);
$attributeGroups = array_merge($attributeGroups, $currentAttributeGroups);
$fullyQualified = new FullyQualified($annotationToAttribute->getAttributeClass());
if ($node instanceof AbstractTagValueNode) {
$args = $this->createArgsFromItems($node->getItemsWithoutDefaults());
} else {
$args = [];
}
return $attributeGroups;
}
/**
* @return Arg[]
*/
public function printItemsToAttributeArgs(PhpAttributableTagNodeInterface $phpAttributableTagNode): array
{
$items = $phpAttributableTagNode->getAttributableItems();
return $this->createArgsFromItems($items);
}
/**
* @return AttributeGroup[]
*/
private function printPhpAttributableTagNode(PhpAttributableTagNodeInterface $phpAttributableTagNode): array
{
$args = $this->printItemsToAttributeArgs($phpAttributableTagNode);
$attributeClassName = $this->resolveAttributeClassName($phpAttributableTagNode);
$attributeGroups = [];
$attributeGroups[] = $this->createAttributeGroupFromNameAndArgs($attributeClassName, $args);
if ($phpAttributableTagNode instanceof ManyPhpAttributableTagNodeInterface) {
foreach ($phpAttributableTagNode->provide() as $shortName => $items) {
$args = $this->createArgsFromItems($items);
$name = new Name($shortName);
$attributeGroups[] = $this->createAttributeGroupFromNameAndArgs($name, $args);
}
}
return $attributeGroups;
$attribute = new Attribute($fullyQualified, $args);
return new AttributeGroup([$attribute]);
}
/**
@ -101,24 +60,6 @@ final class PhpAttributeGroupFactory
return $args;
}
private function resolveAttributeClassName(PhpAttributableTagNodeInterface $phpAttributableTagNode): Name
{
if ($phpAttributableTagNode->getAttributeClassName() !== self::TO_BE_ANNOUNCED) {
return new FullyQualified($phpAttributableTagNode->getAttributeClassName());
}
return new Name($phpAttributableTagNode->getShortName());
}
/**
* @param Arg[] $args
*/
private function createAttributeGroupFromNameAndArgs(Name $name, array $args): AttributeGroup
{
$attribute = new Attribute($name, $args);
return new AttributeGroup([$attribute]);
}
/**
* @param mixed[] $items
*/

View File

@ -1,31 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
use Doctrine\ORM\Mapping as ORM;
class IdColumnGeneratedvalue
{
/**
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
public $name;
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
use Doctrine\ORM\Mapping as ORM;
class IdColumnGeneratedvalue
{
#[@ORM\Column(type: 'integer')]
#[@ORM\GeneratedValue]
public $name;
}
?>

View File

@ -1,34 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
use Doctrine\ORM\Mapping as ORM;
class JoinTable
{
/**
* @ORM\JoinTable(name="users_phonenumbers",
* joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="phonenumber_id", referencedColumnName="id", unique=true)}
* )
*/
public $name;
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
use Doctrine\ORM\Mapping as ORM;
class JoinTable
{
#[@ORM\JoinTable(name: 'users_phonenumbers')]
#[@ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
#[@ORM\InverseJoinColumn(name: 'phonenumber_id', referencedColumnName: 'id', unique: true)]
public $name;
}
?>

View File

@ -1,29 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
use Doctrine\ORM\Mapping as ORM;
class JustId
{
/**
* @ORM\Id
*/
public $name;
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
use Doctrine\ORM\Mapping as ORM;
class JustId
{
#[@ORM\Id]
public $name;
}
?>

View File

@ -1,29 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
use Doctrine\ORM\Mapping as ORM;
class ManyToMany
{
/**
* @ORM\ManyToMany(targetEntity="Phonenumber")
*/
public $name;
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
use Doctrine\ORM\Mapping as ORM;
class ManyToMany
{
#[@ORM\ManyToMany(targetEntity: 'Phonenumber')]
public $name;
}
?>

View File

@ -2,13 +2,11 @@
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
class EntityColumnAndAssertEmail
{
/**
* @ORM\Column(type="string", unique=true)
* @Assert\Email(message="The email '{{ value }}' is not a valid email.")
*/
public $name;
@ -20,12 +18,10 @@ class EntityColumnAndAssertEmail
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
class EntityColumnAndAssertEmail
{
#[@ORM\Column(type: 'string', unique: true)]
#[\Symfony\Component\Validator\Constraints\Email(message: 'This value is not a valid email address.')]
public $name;
}

View File

@ -1,10 +1,10 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Symfony\Component\Validator\Constraints as Assert;
class AssertRange
final class AssertRange
{
/**
* @Assert\Range(
@ -21,11 +21,11 @@ class AssertRange
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\RFC;
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Symfony\Component\Validator\Constraints as Assert;
class AssertRange
final class AssertRange
{
#[\Symfony\Component\Validator\Constraints\Range(min: 120, max: 180, minMessage: 'You must be at least {{ limit }}cm tall to enter', maxMessage: 'You cannot be taller than {{ limit }}cm to enter')]
protected $height;

View File

@ -1,27 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="MyProject\UserRepository", readOnly=true)
*/
class EntityWithRepository
{
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
#[@ORM\Entity(repositoryClass: 'MyProject\UserRepository', readOnly: true)]
class EntityWithRepository
{
}
?>

View File

@ -1,27 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class SomeClass
{
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
#[@ORM\Entity]
class SomeClass
{
}
?>

View File

@ -1,29 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
final class NetteCrossOrigin
{
/**
* @crossOrigin
*/
public function run()
{
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
final class NetteCrossOrigin
{
#[\Nette\Application\Attributes\CrossOrigin]
public function run()
{
}
}
?>

View File

@ -3,13 +3,11 @@
declare(strict_types=1);
use Rector\Core\Configuration\Option;
use Rector\Php80\Rector\Class_\AnnotationToAttributeRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->import(__DIR__ . '/configured_rule.php');
$parameters = $containerConfigurator->parameters();
$parameters->set(Option::AUTO_IMPORT_NAMES, true);
$services = $containerConfigurator->services();
$services->set(AnnotationToAttributeRector::class);
};

View File

@ -2,11 +2,47 @@
declare(strict_types=1);
use Rector\BetterPhpDocParser\ValueObject\PhpDoc\SymfonyRequiredTagNode;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Symfony\SymfonyRouteTagValueNode;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Symfony\Validator\Constraints\AssertEmailTagValueNode;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Symfony\Validator\Constraints\AssertRangeTagValueNode;
use Rector\Nette\PhpDoc\Node\NetteCrossOriginTagNode;
use Rector\Nette\PhpDoc\Node\NetteInjectTagNode;
use Rector\Nette\PhpDoc\Node\NettePersistentTagNode;
use Rector\Php80\Rector\Class_\AnnotationToAttributeRector;
use Rector\Php80\ValueObject\AnnotationToAttribute;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symplify\SymfonyPhpConfig\ValueObjectInliner;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(AnnotationToAttributeRector::class)
->call('configure', [[
AnnotationToAttributeRector::ANNOTATION_TO_ATTRIBUTE => ValueObjectInliner::inline([
// nette 3.0+, see https://github.com/nette/application/commit/d2471134ed909210de8a3e8559931902b1bee67b#diff-457507a8bdc046dd4f3a4aa1ca51794543fbb1e06f03825ab69ee864549a570c
new AnnotationToAttribute(NetteInjectTagNode::class, 'Nette\DI\Attributes\Inject'),
new AnnotationToAttribute(NettePersistentTagNode::class, 'Nette\Application\Attributes\Persistent'),
new AnnotationToAttribute(NetteCrossOriginTagNode::class, 'Nette\Application\Attributes\CrossOrigin'),
$services->set(AnnotationToAttributeRector::class);
// symfony
new AnnotationToAttribute(
SymfonyRequiredTagNode::class,
'Symfony\Contracts\Service\Attribute\Required'
),
new AnnotationToAttribute(
SymfonyRouteTagValueNode::class,
'Symfony\Component\Routing\Annotation\Route'
),
// symfony/validation
new AnnotationToAttribute(
AssertEmailTagValueNode::class,
'Symfony\Component\Validator\Constraints\Email'
),
new AnnotationToAttribute(
AssertRangeTagValueNode::class,
'Symfony\Component\Validator\Constraints\Range'
),
]),
]]);
};

View File

@ -34,11 +34,11 @@ final class DoctrineItemDefaultValueManipulator
string $item,
$defaultValue
): bool {
$attributableItems = $doctrineTagValueNode->getAttributableItems();
if (! isset($attributableItems[$item])) {
$items = $doctrineTagValueNode->getItems();
if (! isset($items[$item])) {
return false;
}
return $attributableItems[$item] === $defaultValue;
return $items[$item] === $defaultValue;
}
}

View File

@ -11,10 +11,17 @@ use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Property;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover;
use Rector\BetterPhpDocParser\ValueObject\PhpDocNode\Symfony\SymfonyRouteTagValueNode;
use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\PhpAttribute\AnnotationToAttributeConverter;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Php80\ValueObject\AnnotationToAttribute;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Webmozart\Assert\Assert;
/**
* @see https://wiki.php.net/rfc/attributes_v2
@ -23,22 +30,40 @@ use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
*
* @see \Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\AnnotationToAttributeRectorTest
*/
final class AnnotationToAttributeRector extends AbstractRector
final class AnnotationToAttributeRector extends AbstractRector implements ConfigurableRectorInterface
{
/**
* @var AnnotationToAttributeConverter
* @var string
*/
private $annotationToAttributeConverter;
public const ANNOTATION_TO_ATTRIBUTE = 'annotation_to_attribute';
public function __construct(AnnotationToAttributeConverter $annotationToAttributeConverter)
{
$this->annotationToAttributeConverter = $annotationToAttributeConverter;
/**
* @var AnnotationToAttribute[]
*/
private $annotationsToAttributes = [];
/**
* @var PhpAttributeGroupFactory
*/
private $phpAttributeGroupFactory;
/**
* @var PhpDocTagRemover
*/
private $phpDocTagRemover;
public function __construct(
PhpAttributeGroupFactory $phpAttributeGroupFactory,
PhpDocTagRemover $phpDocTagRemover
) {
$this->phpAttributeGroupFactory = $phpAttributeGroupFactory;
$this->phpDocTagRemover = $phpDocTagRemover;
}
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Change annotation to attribute', [
new CodeSample(
new ConfiguredCodeSample(
<<<'CODE_SAMPLE'
use Symfony\Component\Routing\Annotation\Route;
@ -64,6 +89,14 @@ class SymfonyRoute
}
}
CODE_SAMPLE
, [
self::ANNOTATION_TO_ATTRIBUTE => [
new AnnotationToAttribute(
SymfonyRouteTagValueNode::class,
'Symfony\Component\Routing\Annotation\Route'
),
],
]
),
]);
}
@ -88,6 +121,48 @@ CODE_SAMPLE
*/
public function refactor(Node $node): ?Node
{
return $this->annotationToAttributeConverter->convertNode($node);
if (! $this->isAtLeastPhpVersion(PhpVersionFeature::ATTRIBUTES)) {
return null;
}
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($node);
if (! $phpDocInfo instanceof PhpDocInfo) {
return null;
}
$hasNewAttrGroups = false;
foreach ($this->annotationsToAttributes as $annotationToAttribute) {
$tagNodeType = $annotationToAttribute->getTagNodeClass();
$phpDocTagNode = $phpDocInfo->getByType($tagNodeType);
if (! $phpDocTagNode instanceof \PHPStan\PhpDocParser\Ast\Node) {
continue;
}
// 1. remove php-doc tag
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $phpDocTagNode);
// 2. add attributes
$node->attrGroups[] = $this->phpAttributeGroupFactory->create($phpDocTagNode, $annotationToAttribute);
$hasNewAttrGroups = true;
}
if ($hasNewAttrGroups) {
return $node;
}
return null;
}
/**
* @param array<string, AnnotationToAttribute[]> $configuration
*/
public function configure(array $configuration): void
{
$annotationsToAttributes = $configuration[self::ANNOTATION_TO_ATTRIBUTE] ?? [];
Assert::allIsInstanceOf($annotationsToAttributes, AnnotationToAttribute::class);
$this->annotationsToAttributes = $annotationsToAttributes;
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Rector\Php80\ValueObject;
use PHPStan\PhpDocParser\Ast\Node;
final class AnnotationToAttribute
{
/**
* @var class-string<Node>
*/
private $tagNodeClass;
/**
* @var class-string
*/
private $attributeClass;
/**
* @param class-string<Node> $tagNodeClass
* @param class-string $attributeClass
*/
public function __construct(string $tagNodeClass, string $attributeClass)
{
$this->tagNodeClass = $tagNodeClass;
$this->attributeClass = $attributeClass;
}
/**
* @return class-string<Node>
*/
public function getTagNodeClass(): string
{
return $this->tagNodeClass;
}
/**
* @return class-string
*/
public function getAttributeClass(): string
{
return $this->attributeClass;
}
}

View File

@ -188,4 +188,10 @@ final class PhpVersionFeature
* @var int
*/
public const PROPERTY_PROMOTION = PhpVersion::PHP_80;
/**
* @see https://wiki.php.net/rfc/attributes_v2
* @var int
*/
public const ATTRIBUTES = PhpVersion::PHP_80;
}