add Symfony Validator annotations

This commit is contained in:
Tomas Votruba 2019-08-30 21:41:21 +02:00
parent 2b51b257e1
commit 04d01ffdfe
30 changed files with 798 additions and 129 deletions

View File

@ -73,7 +73,7 @@ CODE_SAMPLE
$phpDocInfo = $this->docBlockManipulator->createPhpDocInfoFromNode($node);
$doctrineEntityTag = $phpDocInfo->getDoctrineEntityTag();
$doctrineEntityTag = $phpDocInfo->getDoctrineEntity();
if ($doctrineEntityTag === null) {
return null;
}

View File

@ -177,49 +177,49 @@ final class PhpDocInfo
return $this->getResolvedTypesAttribute($varTagValue);
}
public function getDoctrineIdTagValueNode(): ?IdTagValueNode
public function getDoctrineId(): ?IdTagValueNode
{
return $this->matchChildValueNodeOfType(IdTagValueNode::class);
return $this->getByType(IdTagValueNode::class);
}
public function getDoctrineTableTagValueNode(): ?TableTagValueNode
public function getDoctrineTable(): ?TableTagValueNode
{
return $this->matchChildValueNodeOfType(TableTagValueNode::class);
return $this->getByType(TableTagValueNode::class);
}
public function getDoctrineManyToManyTagValueNode(): ?ManyToManyTagValueNode
public function getDoctrineManyToMany(): ?ManyToManyTagValueNode
{
return $this->matchChildValueNodeOfType(ManyToManyTagValueNode::class);
return $this->getByType(ManyToManyTagValueNode::class);
}
public function getDoctrineManyToOneTagValueNode(): ?ManyToOneTagValueNode
public function getDoctrineManyToOne(): ?ManyToOneTagValueNode
{
return $this->matchChildValueNodeOfType(ManyToOneTagValueNode::class);
return $this->getByType(ManyToOneTagValueNode::class);
}
public function getDoctrineOneToOneTagValueNode(): ?OneToOneTagValueNode
public function getDoctrineOneToOne(): ?OneToOneTagValueNode
{
return $this->matchChildValueNodeOfType(OneToOneTagValueNode::class);
return $this->getByType(OneToOneTagValueNode::class);
}
public function getDoctrineOneToManyTagValueNode(): ?OneToManyTagValueNode
public function getDoctrineOneToMany(): ?OneToManyTagValueNode
{
return $this->matchChildValueNodeOfType(OneToManyTagValueNode::class);
return $this->getByType(OneToManyTagValueNode::class);
}
public function getDoctrineEntityTag(): ?EntityTagValueNode
public function getDoctrineEntity(): ?EntityTagValueNode
{
return $this->matchChildValueNodeOfType(EntityTagValueNode::class);
return $this->getByType(EntityTagValueNode::class);
}
public function getDoctrineColumnTagValueNode(): ?ColumnTagValueNode
public function getDoctrineColumn(): ?ColumnTagValueNode
{
return $this->matchChildValueNodeOfType(ColumnTagValueNode::class);
return $this->getByType(ColumnTagValueNode::class);
}
public function getDoctrineJoinColumnTagValueNode(): ?JoinColumnTagValueNode
{
return $this->matchChildValueNodeOfType(JoinColumnTagValueNode::class);
return $this->getByType(JoinColumnTagValueNode::class);
}
/**
@ -263,10 +263,10 @@ final class PhpDocInfo
public function getDoctrineRelationTagValueNode(): ?DoctrineRelationTagValueNodeInterface
{
return $this->getDoctrineManyToManyTagValueNode() ??
$this->getDoctrineOneToManyTagValueNode() ??
$this->getDoctrineOneToOneTagValueNode() ??
$this->getDoctrineManyToOneTagValueNode() ?? null;
return $this->getDoctrineManyToMany() ??
$this->getDoctrineOneToMany() ??
$this->getDoctrineOneToOne() ??
$this->getDoctrineManyToOne() ?? null;
}
public function removeTagValueNodeFromNode(PhpDocTagValueNode $phpDocTagValueNode): void
@ -285,7 +285,7 @@ final class PhpDocInfo
/**
* @param string $type
*/
public function matchChildValueNodeOfType(string $type): ?PhpDocTagValueNode
public function getByType(string $type): ?PhpDocTagValueNode
{
foreach ($this->phpDocNode->children as $phpDocChildNode) {
if ($phpDocChildNode instanceof PhpDocTagNode) {

View File

@ -7,11 +7,17 @@ use Nette\Utils\Strings;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use Rector\BetterPhpDocParser\Attributes\Attribute\AttributeTrait;
use Rector\BetterPhpDocParser\Attributes\Contract\Ast\AttributeAwareNodeInterface;
use Rector\DoctrinePhpDocParser\Array_\ArrayItemStaticHelper;
abstract class AbstractTagValueNode implements AttributeAwareNodeInterface, PhpDocTagValueNode
{
use AttributeTrait;
/**
* @var string[]|null
*/
protected $orderedVisibleItems;
/**
* @param mixed[] $item
*/
@ -21,6 +27,51 @@ abstract class AbstractTagValueNode implements AttributeAwareNodeInterface, PhpD
$json = Strings::replace($json, '#,#', ', ');
$json = Strings::replace($json, '#\[(.*?)\]#', '{$1}');
// cleanup json encoded extra slashes
$json = Strings::replace($json, '#\\\\\\\\#', '\\');
return sprintf('%s=%s', $key, $json);
}
/**
* @param string[] $contentItems
*/
protected function printContentItems(array $contentItems): string
{
if ($this->orderedVisibleItems !== null) {
$contentItems = ArrayItemStaticHelper::filterAndSortVisibleItems($contentItems, $this->orderedVisibleItems);
}
if ($contentItems === []) {
return '';
}
return '(' . implode(', ', $contentItems) . ')';
}
/**
* @param PhpDocTagValueNode[] $tagValueNodes
*/
protected function printTagValueNodesSeparatedByComma(array $tagValueNodes, string $prefix = ''): string
{
if ($tagValueNodes === []) {
return '';
}
$itemsAsStrings = [];
foreach ($tagValueNodes as $tagValueNode) {
$itemsAsStrings[] = $prefix . (string) $tagValueNode;
}
return implode(', ', $itemsAsStrings);
}
protected function resolveItemsOrderFromAnnotationContent(?string $annotationContent): void
{
if ($annotationContent === null) {
return;
}
$this->orderedVisibleItems = ArrayItemStaticHelper::resolveAnnotationItemsOrder($annotationContent);
}
}

View File

@ -119,7 +119,7 @@ final class BetterPhpDocParser extends PhpDocParser
$tokenIterator->next();
// @todo somehow decouple to tag pre-processor
if ($tag === '@ORM') {
if (Strings::match($tag, '#@(ORM|Assert|Serializer)$#')) {
$tag .= $tokenIterator->currentTokenValue();
$tokenIterator->next();
}

View File

@ -29,7 +29,7 @@ final class DoctrineDocBlockResolver
return false;
}
return (bool) $classPhpDocInfo->getDoctrineEntityTag();
return (bool) $classPhpDocInfo->getDoctrineEntity();
}
public function getTargetEntity(Property $property): ?string
@ -49,7 +49,7 @@ final class DoctrineDocBlockResolver
return false;
}
return (bool) $propertyPhpDocInfo->getDoctrineIdTagValueNode();
return (bool) $propertyPhpDocInfo->getDoctrineId();
}
public function getDoctrineRelationTagValueNode(Property $property): ?DoctrineRelationTagValueNodeInterface
@ -69,7 +69,7 @@ final class DoctrineDocBlockResolver
return null;
}
return $classPhpDocInfo->getDoctrineTableTagValueNode();
return $classPhpDocInfo->getDoctrineTable();
}
public function isDoctrineProperty(Property $property): bool
@ -79,7 +79,7 @@ final class DoctrineDocBlockResolver
return false;
}
if ($propertyPhpDocInfo->getDoctrineColumnTagValueNode()) {
if ($propertyPhpDocInfo->getDoctrineColumn()) {
return true;
}

View File

@ -116,13 +116,13 @@ final class AddUuidToEntityWhereMissingRector extends AbstractRector
return false;
}
$idTagValueNode = $propertyPhpDocInfo->getDoctrineIdTagValueNode();
$idTagValueNode = $propertyPhpDocInfo->getDoctrineId();
if ($idTagValueNode === null) {
return false;
}
// get column!
$columnTagValueNode = $propertyPhpDocInfo->getDoctrineColumnTagValueNode();
$columnTagValueNode = $propertyPhpDocInfo->getDoctrineColumn();
if ($columnTagValueNode === null) {
return false;
}

View File

@ -14,6 +14,7 @@ use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\Validator\Constraint;
final class NodeAnnotationReader
{
@ -46,7 +47,7 @@ final class NodeAnnotationReader
return $this->annotationReader->getMethodAnnotation($reflectionMethod, $annotationClassName);
}
public function readDoctrineClassAnnotation(Class_ $class, string $annotationClassName): Annotation
public function readClassAnnotation(Class_ $class, string $annotationClassName): Annotation
{
$classReflection = $this->createClassReflectionFromNode($class);
@ -59,7 +60,10 @@ final class NodeAnnotationReader
return $classAnnotation;
}
public function readDoctrinePropertyAnnotation(Property $property, string $annotationClassName): Annotation
/**
* @return Annotation|Constraint|null
*/
public function readPropertyAnnotation(Property $property, string $annotationClassName)
{
$propertyReflection = $this->createPropertyReflectionFromPropertyNode($property);

View File

@ -2,48 +2,9 @@
namespace Rector\DoctrinePhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use Rector\BetterPhpDocParser\PhpDocParser\Ast\PhpDoc\AbstractTagValueNode;
use Rector\DoctrinePhpDocParser\Array_\ArrayItemStaticHelper;
use Rector\DoctrinePhpDocParser\Contract\Ast\PhpDoc\DoctrineTagNodeInterface;
abstract class AbstractDoctrineTagValueNode extends AbstractTagValueNode implements DoctrineTagNodeInterface
{
/**
* @var string[]|null
*/
protected $orderedVisibleItems;
/**
* @param string[] $contentItems
*/
protected function printContentItems(array $contentItems): string
{
if ($this->orderedVisibleItems !== null) {
$contentItems = ArrayItemStaticHelper::filterAndSortVisibleItems($contentItems, $this->orderedVisibleItems);
}
if ($contentItems === []) {
return '';
}
return '(' . implode(', ', $contentItems) . ')';
}
/**
* @param PhpDocTagValueNode[] $tagValueNodes
*/
protected function printTagValueNodesSeparatedByComma(array $tagValueNodes, string $prefix = ''): string
{
if ($tagValueNodes === []) {
return '';
}
$itemsAsStrings = [];
foreach ($tagValueNodes as $tagValueNode) {
$itemsAsStrings[] = $prefix . (string) $tagValueNode;
}
return implode(', ', $itemsAsStrings);
}
}

View File

@ -133,4 +133,9 @@ final class ManyToManyTagValueNode extends AbstractDoctrineTagValueNode implemen
{
$this->inversedBy = null;
}
public function changeTargetEntity(string $targetEntity): void
{
$this->targetEntity = $targetEntity;
}
}

View File

@ -90,4 +90,9 @@ final class ManyToOneTagValueNode extends AbstractDoctrineTagValueNode implement
{
$this->inversedBy = null;
}
public function changeTargetEntity(string $targetEntity): void
{
$this->targetEntity = $targetEntity;
}
}

View File

@ -107,4 +107,9 @@ final class OneToManyTagValueNode extends AbstractDoctrineTagValueNode implement
{
$this->mappedBy = null;
}
public function changeTargetEntity(string $targetEntity): void
{
$this->targetEntity = $targetEntity;
}
}

View File

@ -126,4 +126,9 @@ final class OneToOneTagValueNode extends AbstractDoctrineTagValueNode implements
{
$this->mappedBy = null;
}
public function changeTargetEntity(string $targetEntity): void
{
$this->targetEntity = $targetEntity;
}
}

View File

@ -7,4 +7,6 @@ interface DoctrineRelationTagValueNodeInterface
public function getTargetEntity(): ?string;
public function getFqnTargetEntity(): ?string;
public function changeTargetEntity(string $targetEntity): void;
}

View File

@ -113,7 +113,7 @@ final class OrmTagParser extends AbstractPhpDocParser
private function createEntityTagValueNode(Class_ $class, string $annotationContent): EntityTagValueNode
{
/** @var Entity $entity */
$entity = $this->nodeAnnotationReader->readDoctrineClassAnnotation($class, Entity::class);
$entity = $this->nodeAnnotationReader->readClassAnnotation($class, Entity::class);
return new EntityTagValueNode($entity->repositoryClass, $entity->readOnly, $this->resolveAnnotationItemsOrder(
$annotationContent
@ -123,7 +123,7 @@ final class OrmTagParser extends AbstractPhpDocParser
private function createTableTagValueNode(Class_ $class, string $annotationContent): TableTagValueNode
{
/** @var Table $table */
$table = $this->nodeAnnotationReader->readDoctrineClassAnnotation($class, Table::class);
$table = $this->nodeAnnotationReader->readClassAnnotation($class, Table::class);
return new TableTagValueNode(
$table->name,
@ -138,7 +138,7 @@ final class OrmTagParser extends AbstractPhpDocParser
private function createColumnTagValueNode(Property $property, string $annotationContent): ColumnTagValueNode
{
/** @var Column $column */
$column = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, Column::class);
$column = $this->nodeAnnotationReader->readPropertyAnnotation($property, Column::class);
return new ColumnTagValueNode(
$column->name,
@ -157,7 +157,7 @@ final class OrmTagParser extends AbstractPhpDocParser
private function createManyToManyTagValueNode(Property $property, string $annotationContent): ManyToManyTagValueNode
{
/** @var ManyToMany $manyToMany */
$manyToMany = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, ManyToMany::class);
$manyToMany = $this->nodeAnnotationReader->readPropertyAnnotation($property, ManyToMany::class);
return new ManyToManyTagValueNode(
$manyToMany->targetEntity,
@ -175,7 +175,7 @@ final class OrmTagParser extends AbstractPhpDocParser
private function createManyToOneTagValueNode(Property $property, string $annotationContent): ManyToOneTagValueNode
{
/** @var ManyToOne $manyToOne */
$manyToOne = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, ManyToOne::class);
$manyToOne = $this->nodeAnnotationReader->readPropertyAnnotation($property, ManyToOne::class);
return new ManyToOneTagValueNode(
$manyToOne->targetEntity,
@ -190,7 +190,7 @@ final class OrmTagParser extends AbstractPhpDocParser
private function createOneToOneTagValueNode(Property $property, string $annotationContent): OneToOneTagValueNode
{
/** @var OneToOne $oneToOne */
$oneToOne = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, OneToOne::class);
$oneToOne = $this->nodeAnnotationReader->readPropertyAnnotation($property, OneToOne::class);
return new OneToOneTagValueNode(
$oneToOne->targetEntity,
@ -207,7 +207,7 @@ final class OrmTagParser extends AbstractPhpDocParser
private function createOneToManyTagValueNode(Property $property, string $annotationContent): OneToManyTagValueNode
{
/** @var OneToMany $oneToMany */
$oneToMany = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, OneToMany::class);
$oneToMany = $this->nodeAnnotationReader->readPropertyAnnotation($property, OneToMany::class);
return new OneToManyTagValueNode(
$oneToMany->mappedBy,
@ -224,7 +224,7 @@ final class OrmTagParser extends AbstractPhpDocParser
private function createJoinColumnTagValueNode(Property $property, string $annotationContent): JoinColumnTagValueNode
{
/** @var JoinColumn $joinColumn */
$joinColumn = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, JoinColumn::class);
$joinColumn = $this->nodeAnnotationReader->readPropertyAnnotation($property, JoinColumn::class);
return $this->createJoinColumnTagValueNodeFromJoinColumnAnnotation($joinColumn, $annotationContent);
}
@ -232,7 +232,7 @@ final class OrmTagParser extends AbstractPhpDocParser
private function createJoinTableTagValeNode(Property $property, string $annotationContent): JoinTableTagValueNode
{
/** @var JoinTable $joinTable */
$joinTable = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, JoinTable::class);
$joinTable = $this->nodeAnnotationReader->readPropertyAnnotation($property, JoinTable::class);
$joinColumnContents = Strings::matchAll(
$annotationContent,

View File

@ -135,7 +135,7 @@ CODE_SAMPLE
$classMethodPhpDocInfo = $this->getPhpDocInfo($classMethod);
/** @var TemplateTagValueNode|null $templateTagValueNode */
$templateTagValueNode = $classMethodPhpDocInfo->matchChildValueNodeOfType(TemplateTagValueNode::class);
$templateTagValueNode = $classMethodPhpDocInfo->getByType(TemplateTagValueNode::class);
if ($templateTagValueNode === null) {
throw new ShouldNotHappenException(__METHOD__);
}

View File

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Rector\Symfony\PhpDocParser\Ast\PhpDoc;
use Rector\BetterPhpDocParser\PhpDocParser\Ast\PhpDoc\AbstractTagValueNode;
abstract class AbstractConstaintTagValueNode extends AbstractTagValueNode
{
}

View File

@ -0,0 +1,63 @@
<?php declare(strict_types=1);
namespace Rector\Symfony\PhpDocParser\Ast\PhpDoc;
use Symfony\Component\Validator\Constraints\Choice;
final class AssertChoiceTagValueNode extends AbstractConstaintTagValueNode
{
/**
* @var string
*/
public const SHORT_NAME = '@Assert\Choice';
/**
* @var string
*/
public const CLASS_NAME = Choice::class;
/**
* @var mixed[]|null
*/
private $callback;
/**
* @var bool|null
*/
private $strict;
/**
* @param mixed[]|null $callback
*/
public function __construct(?array $callback, ?bool $strict, string $annotationContent)
{
$this->callback = $callback;
$this->strict = $strict;
$this->resolveItemsOrderFromAnnotationContent($annotationContent);
}
public function __toString(): string
{
$contentItems = [];
if ($this->callback) {
$contentItems['callback'] = $this->printArrayItem($this->callback, 'callback');
}
if ($this->strict !== null) {
$contentItems['strict'] = sprintf('strict=%s', $this->strict ? 'true' : 'false');
}
return $this->printContentItems($contentItems);
}
public function isCallbackClass(string $class): bool
{
return $class === ($this->callback[0] ?? null);
}
public function changeCallbackClass(string $newClass): void
{
$this->callback[0] = $newClass;
}
}

View File

@ -0,0 +1,49 @@
<?php declare(strict_types=1);
namespace Rector\Symfony\PhpDocParser\Ast\PhpDoc;
use JMS\Serializer\Annotation\Type;
use Nette\Utils\Strings;
use Rector\BetterPhpDocParser\PhpDocParser\Ast\PhpDoc\AbstractTagValueNode;
final class SerializerTypeTagValueNode extends AbstractTagValueNode
{
/**
* @var string
*/
public const SHORT_NAME = '@Serializer\Type';
/**
* @var string
*/
public const CLASS_NAME = Type::class;
/**
* @var string
*/
private $name;
public function __construct(string $name, ?string $annotationContent)
{
$this->name = $name;
$this->resolveItemsOrderFromAnnotationContent($annotationContent);
}
public function __toString(): string
{
return sprintf('("%s")', $this->name);
}
public function replaceName(string $oldName, string $newName): bool
{
$oldNamePattern = '#\b' . preg_quote($oldName, '#') . '\b#';
$newNameValue = Strings::replace($this->name, $oldNamePattern, $newName);
if ($newNameValue !== $this->name) {
$this->name = $newNameValue;
return true;
}
return false;
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types=1);
namespace Rector\Symfony\PhpDocParser\Extension;
use Nette\Utils\Strings;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use Rector\BetterPhpDocParser\Contract\PhpDocParserExtensionInterface;
use Rector\Symfony\PhpDocParser\SymfonyPhpDocTagParser;
final class SymfonyPhpDocParserExtension implements PhpDocParserExtensionInterface
{
/**
* @var SymfonyPhpDocTagParser
*/
private $symfonyPhpDocTagParser;
public function __construct(SymfonyPhpDocTagParser $symfonyPhpDocTagParser)
{
$this->symfonyPhpDocTagParser = $symfonyPhpDocTagParser;
}
public function matchTag(string $tag): bool
{
return (bool) Strings::match($tag, '#^@(Assert|Serializer)\\\\(.*?)$#');
}
public function parse(TokenIterator $tokenIterator, string $tag): ?PhpDocTagValueNode
{
return $this->symfonyPhpDocTagParser->parse($tokenIterator, $tag);
}
}

View File

@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace Rector\Symfony\PhpDocParser;
use JMS\Serializer\Annotation\Type;
use PhpParser\Node\Stmt\Property;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use Rector\BetterPhpDocParser\PhpDocParser\AbstractPhpDocParser;
use Rector\Symfony\PhpDocParser\Ast\PhpDoc\AssertChoiceTagValueNode;
use Rector\Symfony\PhpDocParser\Ast\PhpDoc\SerializerTypeTagValueNode;
use Symfony\Component\Validator\Constraints\Choice;
final class SymfonyPhpDocTagParser extends AbstractPhpDocParser
{
public function parse(TokenIterator $tokenIterator, string $tag): ?PhpDocTagValueNode
{
$currentPhpNode = $this->getCurrentPhpNode();
// this is needed to append tokens to the end of annotation, even if not used
$annotationContent = $this->resolveAnnotationContent($tokenIterator);
if ($currentPhpNode instanceof Property) {
if ($tag === AssertChoiceTagValueNode::SHORT_NAME) {
return $this->createAssertChoiceTagValueNode($currentPhpNode, $annotationContent);
}
if ($tag === SerializerTypeTagValueNode::SHORT_NAME) {
return $this->createSerializerTypeTagValueNode($currentPhpNode, $annotationContent);
}
}
return null;
}
private function createAssertChoiceTagValueNode(
Property $property,
string $annotationContent
): AssertChoiceTagValueNode {
/** @var Choice $choiceAnnotation */
$choiceAnnotation = $this->nodeAnnotationReader->readPropertyAnnotation(
$property,
AssertChoiceTagValueNode::CLASS_NAME
);
return new AssertChoiceTagValueNode($choiceAnnotation->callback, $choiceAnnotation->strict, $annotationContent);
}
private function createSerializerTypeTagValueNode(
Property $property,
string $annotationContent
): SerializerTypeTagValueNode {
/** @var Type $typeAnnotation */
$typeAnnotation = $this->nodeAnnotationReader->readPropertyAnnotation(
$property,
SerializerTypeTagValueNode::CLASS_NAME
);
return new SerializerTypeTagValueNode($typeAnnotation->name, $annotationContent);
}
}

View File

@ -68,7 +68,7 @@ final class DoctrineColumnPropertyTypeInferer implements PropertyTypeInfererInte
}
$phpDocInfo = $this->docBlockManipulator->createPhpDocInfoFromNode($property);
$doctrineColumnTagValueNode = $phpDocInfo->getDoctrineColumnTagValueNode();
$doctrineColumnTagValueNode = $phpDocInfo->getDoctrineColumn();
if ($doctrineColumnTagValueNode === null) {
return [];
}

View File

@ -2,12 +2,38 @@
namespace Rector\PhpDoc;
use Nette\Utils\Strings;
use PhpParser\Comment\Doc;
use PhpParser\Node;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\BetterPhpDocParser\Printer\PhpDocInfoPrinter;
use Rector\DoctrinePhpDocParser\Contract\Ast\PhpDoc\DoctrineRelationTagValueNodeInterface;
use Rector\Symfony\PhpDocParser\Ast\PhpDoc\AssertChoiceTagValueNode;
use Rector\Symfony\PhpDocParser\Ast\PhpDoc\SerializerTypeTagValueNode;
final class PhpDocClassRenamer
{
/**
* @var PhpDocInfoFactory
*/
private $phpDocInfoFactory;
/**
* @var PhpDocInfoPrinter
*/
private $phpDocInfoPrinter;
/**
* @var bool
*/
private $shouldUpdate = false;
public function __construct(PhpDocInfoFactory $phpDocInfoFactory, PhpDocInfoPrinter $phpDocInfoPrinter)
{
$this->phpDocInfoFactory = $phpDocInfoFactory;
$this->phpDocInfoPrinter = $phpDocInfoPrinter;
}
/**
* Covers annotations like @ORM, @Serializer, @Assert etc
* See https://github.com/rectorphp/rector/issues/1872
@ -21,27 +47,77 @@ final class PhpDocClassRenamer
return;
}
$textDocComment = $docComment->getText();
$this->shouldUpdate = false;
$oldTypes = array_keys($oldToNewClasses);
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($node);
$this->procesAssertChoiceTagValueNode($oldToNewClasses, $phpDocInfo);
$this->procesDoctrineRelationTagValueNode($oldToNewClasses, $phpDocInfo);
$this->processSerializerTypeTagValueNode($oldToNewClasses, $phpDocInfo);
$oldTypesPregQuoted = [];
foreach ($oldTypes as $oldType) {
$oldTypesPregQuoted[] = '\b' . preg_quote($oldType) . '\b';
}
$oldTypesPattern = '#(?|' . implode('|', $oldTypesPregQuoted) . ')#x';
$match = Strings::match($textDocComment, $oldTypesPattern);
if ($match === null) {
if ($this->shouldUpdate === null) {
return;
}
foreach ($match as $matchedOldType) {
$newType = $oldToNewClasses[$matchedOldType];
$textDocComment = Strings::replace($textDocComment, '#\b' . preg_quote($matchedOldType) . '\b#', $newType);
}
$textDocComment = $this->phpDocInfoPrinter->printFormatPreserving($phpDocInfo);
$node->setDocComment(new Doc($textDocComment));
}
/**
* @param string[] $oldToNewClasses
*/
private function procesAssertChoiceTagValueNode(array $oldToNewClasses, PhpDocInfo $phpDocInfo): void
{
$choiceTagValueNode = $phpDocInfo->getByType(AssertChoiceTagValueNode::class);
if (! $choiceTagValueNode instanceof AssertChoiceTagValueNode) {
return;
}
foreach ($oldToNewClasses as $oldClass => $newClass) {
if (! $choiceTagValueNode->isCallbackClass($oldClass)) {
continue;
}
$choiceTagValueNode->changeCallbackClass($newClass);
$this->shouldUpdate = true;
break;
}
}
/**
* @param string[] $oldToNewClasses
*/
private function procesDoctrineRelationTagValueNode(array $oldToNewClasses, PhpDocInfo $phpDocInfo): void
{
$relationTagValueNode = $phpDocInfo->getByType(DoctrineRelationTagValueNodeInterface::class);
if (! $relationTagValueNode instanceof DoctrineRelationTagValueNodeInterface) {
return;
}
foreach ($oldToNewClasses as $oldClass => $newClass) {
if ($relationTagValueNode->getFqnTargetEntity() !== $oldClass) {
continue;
}
$relationTagValueNode->changeTargetEntity($newClass);
$this->shouldUpdate = true;
break;
}
}
/**
* @param string[] $oldToNewClasses
*/
private function processSerializerTypeTagValueNode(array $oldToNewClasses, PhpDocInfo $phpDocInfo): void
{
$serializerTypeTagValueNode = $phpDocInfo->getByType(SerializerTypeTagValueNode::class);
if (! $serializerTypeTagValueNode instanceof SerializerTypeTagValueNode) {
return;
}
foreach ($oldToNewClasses as $oldClass => $newClass) {
if ($serializerTypeTagValueNode->replaceName($oldClass, $newClass)) {
$this->shouldUpdate = true;
}
}
}
}

View File

@ -52,7 +52,6 @@ final class NodeAddingCommander implements CommanderInterface
{
$position = $this->resolveNearestExpressionPosition($positionNode);
// dump($this->wrapToExpression($addedNode));
$this->nodesToAddBefore[$position][] = $this->wrapToExpression($addedNode);
}

View File

@ -150,8 +150,6 @@ CODE_SAMPLE
$propertyName = $this->propertyNaming->fqnToVariableName($serviceType);
// add property via constructor
/** @var Class_ $classNode */
$this->addPropertyToClass($classNode, $serviceType, $propertyName);

View File

@ -0,0 +1,20 @@
<?php declare(strict_types=1);
namespace JMS\Serializer\Annotation;
if (class_exists('JMS\Serializer\Annotation\Type')) {
return;
}
/**
* @Annotation
* @Target({"PROPERTY", "METHOD","ANNOTATION"})
*/
final class Type
{
/**
* @Required
* @var string
*/
public $name;
}

View File

@ -0,0 +1,42 @@
<?php declare(strict_types=1);
namespace Symfony\Component\Validator\Constraints;
if (class_exists('Symfony\Component\Validator\Constraints\Choice')) {
return;
}
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
*/
class Choice extends Constraint
{
const NO_SUCH_CHOICE_ERROR = '8e179f1b-97aa-4560-a02f-2a8b42e49df7';
const TOO_FEW_ERROR = '11edd7eb-5872-4b6e-9f12-89923999fd0e';
const TOO_MANY_ERROR = '9bd98e49-211c-433f-8630-fd1c2d0f08c3';
protected static $errorNames = [
self::NO_SUCH_CHOICE_ERROR => 'NO_SUCH_CHOICE_ERROR',
self::TOO_FEW_ERROR => 'TOO_FEW_ERROR',
self::TOO_MANY_ERROR => 'TOO_MANY_ERROR',
];
public $choices;
public $callback;
public $multiple = false;
public $strict = true;
public $min;
public $max;
public $message = 'The value you selected is not a valid choice.';
public $multipleMessage = 'One or more of the given values is invalid.';
public $minMessage = 'You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.';
public $maxMessage = 'You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.';
/**
* {@inheritdoc}
*/
public function getDefaultOption()
{
return 'choices';
}
}

View File

@ -0,0 +1,262 @@
<?php declare(strict_types=1);
namespace Symfony\Component\Validator;
if (class_exists('Symfony\Component\Validator\Constraint')) {
return;
}
use Exception;
/**
* @property array $groups The groups that the constraint belongs to
*/
abstract class Constraint
{
/**
* The name of the group given to all constraints with no explicit group.
*/
const DEFAULT_GROUP = 'Default';
/**
* Marks a constraint that can be put onto classes.
*/
const CLASS_CONSTRAINT = 'class';
/**
* Marks a constraint that can be put onto properties.
*/
const PROPERTY_CONSTRAINT = 'property';
/**
* Maps error codes to the names of their constants.
*/
protected static $errorNames = [];
/**
* Domain-specific data attached to a constraint.
*
* @var mixed
*/
public $payload;
/**
* Initializes the constraint with options.
*
* You should pass an associative array. The keys should be the names of
* existing properties in this class. The values should be the value for these
* properties.
*
* Alternatively you can override the method getDefaultOption() to return the
* name of an existing property. If no associative array is passed, this
* property is set instead.
*
* You can force that certain options are set by overriding
* getRequiredOptions() to return the names of these options. If any
* option is not set here, an exception is thrown.
*
* @param mixed $options The options (as associative array)
* or the value for the default
* option (any other type)
*
* @throws InvalidOptionsException When you pass the names of non-existing
* options
* @throws MissingOptionsException When you don't pass any of the options
* returned by getRequiredOptions()
* @throws Exception() When you don't pass an associative
* array, but getDefaultOption() returns
* null
*/
public function __construct($options = null)
{
$defaultOption = $this->getDefaultOption();
$invalidOptions = [];
$missingOptions = array_flip((array) $this->getRequiredOptions());
$knownOptions = get_object_vars($this);
// The "groups" option is added to the object lazily
$knownOptions['groups'] = true;
if (\is_array($options) && isset($options['value']) && !property_exists($this, 'value')) {
if (null === $defaultOption) {
throw new Exception(sprintf('No default option is configured for constraint "%s".', \get_class($this)));
}
$options[$defaultOption] = $options['value'];
unset($options['value']);
}
if (\is_array($options)) {
reset($options);
}
if ($options && \is_array($options) && \is_string(key($options))) {
foreach ($options as $option => $value) {
if (\array_key_exists($option, $knownOptions)) {
$this->$option = $value;
unset($missingOptions[$option]);
} else {
$invalidOptions[] = $option;
}
}
} elseif (null !== $options && !(\is_array($options) && 0 === \count($options))) {
if (null === $defaultOption) {
throw new Exception(sprintf('No default option is configured for constraint "%s".', \get_class($this)));
}
if (\array_key_exists($defaultOption, $knownOptions)) {
$this->$defaultOption = $options;
unset($missingOptions[$defaultOption]);
} else {
$invalidOptions[] = $defaultOption;
}
}
if (\count($invalidOptions) > 0) {
throw new InvalidOptionsException(sprintf('The options "%s" do not exist in constraint "%s".', implode('", "', $invalidOptions), \get_class($this)), $invalidOptions);
}
if (\count($missingOptions) > 0) {
throw new MissingOptionsException(sprintf('The options "%s" must be set for constraint "%s".', implode('", "', array_keys($missingOptions)), \get_class($this)), array_keys($missingOptions));
}
}
/**
* Sets the value of a lazily initialized option.
*
* Corresponding properties are added to the object on first access. Hence
* this method will be called at most once per constraint instance and
* option name.
*
* @param string $option The option name
* @param mixed $value The value to set
*
* @throws InvalidOptionsException If an invalid option name is given
*/
public function __set($option, $value)
{
if ('groups' === $option) {
$this->groups = (array) $value;
return;
}
throw new InvalidOptionsException(sprintf('The option "%s" does not exist in constraint "%s".', $option, \get_class($this)), [$option]);
}
/**
* Returns the value of a lazily initialized option.
*
* Corresponding properties are added to the object on first access. Hence
* this method will be called at most once per constraint instance and
* option name.
*
* @param string $option The option name
*
* @return mixed The value of the option
*
* @throws InvalidOptionsException If an invalid option name is given
*
* @internal this method should not be used or overwritten in userland code
*/
public function __get($option)
{
if ('groups' === $option) {
$this->groups = [self::DEFAULT_GROUP];
return $this->groups;
}
throw new InvalidOptionsException(sprintf('The option "%s" does not exist in constraint "%s".', $option, \get_class($this)), [$option]);
}
/**
* @param string $option The option name
*
* @return bool
*/
public function __isset($option)
{
return 'groups' === $option;
}
/**
* Adds the given group if this constraint is in the Default group.
*
* @param string $group
*/
public function addImplicitGroupName($group)
{
if (\in_array(self::DEFAULT_GROUP, $this->groups) && !\in_array($group, $this->groups)) {
$this->groups[] = $group;
}
}
/**
* Returns the name of the default option.
*
* Override this method to define a default option.
*
* @return string|null
*
* @see __construct()
*/
public function getDefaultOption()
{
return null;
}
/**
* Returns the name of the required options.
*
* Override this method if you want to define required options.
*
* @return array
*
* @see __construct()
*/
public function getRequiredOptions()
{
return [];
}
/**
* Returns the name of the class that validates this constraint.
*
* By default, this is the fully qualified name of the constraint class
* suffixed with "Validator". You can override this method to change that
* behavior.
*
* @return string
*/
public function validatedBy()
{
return \get_class($this).'Validator';
}
/**
* Returns whether the constraint can be put onto classes, properties or
* both.
*
* This method should return one or more of the constants
* Constraint::CLASS_CONSTRAINT and Constraint::PROPERTY_CONSTRAINT.
*
* @return string|array One or more constant values
*/
public function getTargets()
{
return self::PROPERTY_CONSTRAINT;
}
/**
* Optimizes the serialized value to minimize storage space.
*
* @internal
*/
public function __sleep(): array
{
// Initialize "groups" option if it is not set
$this->groups;
return array_keys(get_object_vars($this));
}
}

View File

@ -23,12 +23,6 @@ class ClassAnnotations
*/
public $keepThis;
/**
* @Serializer\Type("array<Rector\Tests\Rector\Class_\RenameClassRector\Source\OldClass>")
* @Serializer\Type("iterable<key, Rector\Tests\Rector\Class_\RenameClassRector\Source\OldClass>")
*/
public $flights = [];
/**
* @ORM\OneToMany(targetEntity="Rector\Tests\Rector\Class_\RenameClassRector\Source\OldClass")
*/
@ -62,12 +56,6 @@ class ClassAnnotations
*/
public $keepThis;
/**
* @Serializer\Type("array<Rector\Tests\Rector\Class_\RenameClassRector\Source\NewClass>")
* @Serializer\Type("iterable<key, Rector\Tests\Rector\Class_\RenameClassRector\Source\NewClass>")
*/
public $flights = [];
/**
* @ORM\OneToMany(targetEntity="Rector\Tests\Rector\Class_\RenameClassRector\Source\NewClass")
*/

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\Tests\Rector\Class_\RenameClassRector\Fixture;
use JMS\Serializer\Annotation as Serializer;
class ClassAnnotationsSerializerIterableType
{
/**
* @Serializer\Type("array<Rector\Tests\Rector\Class_\RenameClassRector\Source\OldClass>")
*/
public $flights = [];
}
?>
-----
<?php
namespace Rector\Tests\Rector\Class_\RenameClassRector\Fixture;
use JMS\Serializer\Annotation as Serializer;
class ClassAnnotationsSerializerIterableType
{
/**
* @Serializer\Type("array<Rector\Tests\Rector\Class_\RenameClassRector\Source\NewClass>")
*/
public $flights = [];
}
?>

View File

@ -28,24 +28,25 @@ final class RenameClassRectorTest extends AbstractRectorTestCase
public function provideTestFiles(): Iterator
{
// yield [__DIR__ . '/Fixture/class_to_new.php.inc'];
// yield [__DIR__ . '/Fixture/class_to_interface.php.inc'];
// yield [__DIR__ . '/Fixture/interface_to_class.php.inc'];
// yield [__DIR__ . '/Fixture/name_insensitive.php.inc'];
// yield [__DIR__ . '/Fixture/twig_case.php.inc'];
// yield [__DIR__ . '/Fixture/underscore_doc.php.inc'];
// yield [__DIR__ . '/Fixture/keep_return_tag.php.inc'];
//
// // Renaming class itself and its namespace
// yield [__DIR__ . '/Fixture/rename_class_without_namespace.php.inc'];
// yield [__DIR__ . '/Fixture/rename_class.php.inc'];
// yield [__DIR__ . '/Fixture/rename_interface.php.inc'];
// yield [__DIR__ . '/Fixture/rename_trait.php.inc'];
// yield [__DIR__ . '/Fixture/rename_class_without_namespace_to_class_without_namespace.php.inc'];
// yield [__DIR__ . '/Fixture/rename_class_to_class_without_namespace.php.inc'];
yield [__DIR__ . '/Fixture/class_to_new.php.inc'];
yield [__DIR__ . '/Fixture/class_to_interface.php.inc'];
yield [__DIR__ . '/Fixture/interface_to_class.php.inc'];
yield [__DIR__ . '/Fixture/name_insensitive.php.inc'];
yield [__DIR__ . '/Fixture/twig_case.php.inc'];
yield [__DIR__ . '/Fixture/underscore_doc.php.inc'];
yield [__DIR__ . '/Fixture/keep_return_tag.php.inc'];
// Renaming class itself and its namespace
yield [__DIR__ . '/Fixture/rename_class_without_namespace.php.inc'];
yield [__DIR__ . '/Fixture/rename_class.php.inc'];
yield [__DIR__ . '/Fixture/rename_interface.php.inc'];
yield [__DIR__ . '/Fixture/rename_trait.php.inc'];
yield [__DIR__ . '/Fixture/rename_class_without_namespace_to_class_without_namespace.php.inc'];
yield [__DIR__ . '/Fixture/rename_class_to_class_without_namespace.php.inc'];
// Symfony/Validator + Doctrine + JMS/Serializer annotations
yield [__DIR__ . '/Fixture/class_annotations.php.inc'];
yield [__DIR__ . '/Fixture/class_annotations_serializer_type.php.inc'];
}
/**