[BetterPhpDocParser] Add support for parsing Doctrine annotations

This commit is contained in:
Tomas Votruba 2019-08-26 11:37:02 +02:00
parent f517abb413
commit 42cbfd8905
41 changed files with 830 additions and 58 deletions

View File

@ -12,7 +12,9 @@
"require": {
"php": "^7.1",
"composer/xdebug-handler": "^1.3",
"doctrine/annotations": "^1.7",
"doctrine/inflector": "^1.3",
"doctrine/orm": "^2.6",
"jean85/pretty-package-versions": "^1.2",
"nette/robot-loader": "^3.1",
"nette/utils": "^2.5|^3.0",
@ -52,6 +54,7 @@
"Rector\\ConsoleDiffer\\": "packages/ConsoleDiffer/src",
"Rector\\DeadCode\\": "packages/DeadCode/src",
"Rector\\Doctrine\\": "packages/Doctrine/src",
"Rector\\DoctrinePhpDocParser\\": "packages/DoctrinePhpDocParser/src",
"Rector\\DomainDrivenDesign\\": "packages/DomainDrivenDesign/src",
"Rector\\ElasticSearchDSL\\": "packages/ElasticSearchDSL/src",
"Rector\\FileSystemRector\\": "packages/FileSystemRector/src",
@ -97,6 +100,7 @@
"Rector\\CodingStyle\\Tests\\": "packages/CodingStyle/tests",
"Rector\\DeadCode\\Tests\\": "packages/DeadCode/tests",
"Rector\\Doctrine\\Tests\\": "packages/Doctrine/tests",
"Rector\\DoctrinePhpDocParser\\Tests\\": "packages/DoctrinePhpDocParser/tests",
"Rector\\DomainDrivenDesign\\Tests\\": "packages/DomainDrivenDesign/tests",
"Rector\\ElasticSearchDSL\\Tests\\": "packages/ElasticSearchDSL/tests",
"Rector\\Guzzle\\Tests\\": "packages/Guzzle/tests",

View File

@ -96,6 +96,8 @@ parameters:
- 'src/Util/*.php'
- 'packages/BetterPhpDocParser/src/Annotation/AnnotationNaming.php'
- 'src/Testing/PHPUnit/PHPUnitEnvironment.php'
# honesty first
- 'src/*StaticHelper.php'
Symplify\CodingStandard\Fixer\Naming\PropertyNameMatchingTypeFixer:
- 'packages/NodeTypeResolver/src/PHPStan/Scope/NodeScopeResolver.php'

View File

@ -48,8 +48,8 @@ use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\Type\AttributeAwareNullableT
use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\Type\AttributeAwareThisTypeNode;
use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\Type\AttributeAwareUnionTypeNode;
use Rector\BetterPhpDocParser\Attributes\Contract\Ast\AttributeAwareNodeInterface;
use Rector\BetterPhpDocParser\Exception\NotImplementedYetException;
use Rector\BetterPhpDocParser\Exception\ShouldNotHappenException;
use Rector\Exception\NotImplementedYetException;
use Rector\Exception\ShouldNotHappenException;
final class AttributeAwareNodeFactory
{
@ -125,7 +125,8 @@ final class AttributeAwareNodeFactory
$typeNode,
$phpDocTagValueNode->isVariadic,
$phpDocTagValueNode->parameterName,
$phpDocTagValueNode->description
$phpDocTagValueNode->description,
false // @todo maybe solve better
);
}

View File

@ -1,9 +0,0 @@
<?php declare(strict_types=1);
namespace Rector\BetterPhpDocParser\Exception;
use Exception;
final class ShouldNotHappenException extends Exception
{
}

View File

@ -10,6 +10,8 @@ use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\AttributeAwarePhpDocNode;
use Rector\BetterPhpDocParser\Attributes\Attribute\Attribute;
use Rector\BetterPhpDocParser\Attributes\Contract\Ast\AttributeAwareNodeInterface;
use Rector\BetterPhpDocParser\Contract\PhpDocNodeDecoratorInterface;
use Rector\BetterPhpDocParser\PhpDocParser\OrmTagParser;
use Rector\Configuration\CurrentNodeProvider;
final class PhpDocInfoFactory
{
@ -28,21 +30,31 @@ final class PhpDocInfoFactory
*/
private $lexer;
/**
* @var CurrentNodeProvider
*/
private $currentNodeProvider;
/**
* @param PhpDocNodeDecoratorInterface[] $phpDocNodeDecoratorInterfacenodeDecorators
*/
public function __construct(
PhpDocParser $phpDocParser,
Lexer $lexer,
array $phpDocNodeDecoratorInterfacenodeDecorators
array $phpDocNodeDecoratorInterfacenodeDecorators,
CurrentNodeProvider $currentNodeProvider
) {
$this->phpDocParser = $phpDocParser;
$this->lexer = $lexer;
$this->phpDocNodeDecoratorInterfaces = $phpDocNodeDecoratorInterfacenodeDecorators;
$this->currentNodeProvider = $currentNodeProvider;
}
public function createFromNode(Node $node): PhpDocInfo
{
/** needed for @see OrmTagParser */
$this->currentNodeProvider->setNode($node);
$content = $node->getDocComment()->getText();
$tokens = $this->lexer->tokenize($content);

View File

@ -7,6 +7,7 @@ use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
@ -20,6 +21,7 @@ use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\AttributeAwarePhpDocNode;
use Rector\BetterPhpDocParser\Attributes\Attribute\Attribute;
use Rector\BetterPhpDocParser\Data\StartEndInfo;
use Rector\BetterPhpDocParser\Printer\MultilineSpaceFormatPreserver;
use Rector\DoctrinePhpDocParser\PhpDocParser\OrmTagParser;
use Symplify\PackageBuilder\Reflection\PrivatesAccessor;
use Symplify\PackageBuilder\Reflection\PrivatesCaller;
@ -50,11 +52,17 @@ final class BetterPhpDocParser extends PhpDocParser
*/
private $multilineSpaceFormatPreserver;
/**
* @var OrmTagParser
*/
private $ormTagParser;
public function __construct(
TypeParser $typeParser,
ConstExprParser $constExprParser,
AttributeAwareNodeFactory $attributeAwareNodeFactory,
MultilineSpaceFormatPreserver $multilineSpaceFormatPreserver
MultilineSpaceFormatPreserver $multilineSpaceFormatPreserver,
OrmTagParser $ormTagParser
) {
parent::__construct($typeParser, $constExprParser);
@ -62,6 +70,7 @@ final class BetterPhpDocParser extends PhpDocParser
$this->privatesAccessor = new PrivatesAccessor();
$this->attributeAwareNodeFactory = $attributeAwareNodeFactory;
$this->multilineSpaceFormatPreserver = $multilineSpaceFormatPreserver;
$this->ormTagParser = $ormTagParser;
}
/**
@ -101,8 +110,28 @@ final class BetterPhpDocParser extends PhpDocParser
return $this->attributeAwareNodeFactory->createFromNode($phpDocNode);
}
public function parseTag(TokenIterator $tokenIterator): PhpDocTagNode
{
$tag = $tokenIterator->currentTokenValue();
$tokenIterator->next();
if ($tag === '@ORM') {
$tag .= $tokenIterator->currentTokenValue();
$tokenIterator->next();
}
$value = $this->parseTagValue($tokenIterator, $tag);
return new PhpDocTagNode($tag, $value);
}
public function parseTagValue(TokenIterator $tokenIterator, string $tag): PhpDocTagValueNode
{
// @todo do via extension :)
if (in_array($tag, ['@ORM\Entity', '@ORM\Column'], true)) {
return $this->ormTagParser->parse($tokenIterator, $tag);
}
// needed for reference support in params, see https://github.com/rectorphp/rector/issues/1734
if ($tag === '@param') {
try {

View File

@ -4,8 +4,10 @@ namespace Rector\BetterPhpDocParser\Printer;
use Nette\Utils\Arrays;
use Nette\Utils\Strings;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Lexer\Lexer;
use Rector\BetterPhpDocParser\Data\StartEndInfo;
use Rector\DoctrinePhpDocParser\Contract\Ast\PhpDoc\DoctrineTagNodeInterface;
final class OriginalSpacingRestorer
{
@ -13,11 +15,12 @@ final class OriginalSpacingRestorer
* @param mixed[] $tokens
*/
public function restoreInOutputWithTokensStartAndEndPosition(
Node $node,
string $nodeOutput,
array $tokens,
StartEndInfo $startEndInfo
): string {
$oldWhitespaces = $this->detectOldWhitespaces($tokens, $startEndInfo);
$oldWhitespaces = $this->detectOldWhitespaces($node, $tokens, $startEndInfo);
// no original whitespaces, return
if ($oldWhitespaces === []) {
@ -26,7 +29,6 @@ final class OriginalSpacingRestorer
$newNodeOutput = '';
$i = 0;
// replace system whitespace by old ones
foreach (Strings::split($nodeOutput, '#\s+#') as $nodeOutputPart) {
$newNodeOutput .= ($oldWhitespaces[$i] ?? '') . $nodeOutputPart;
@ -41,11 +43,16 @@ final class OriginalSpacingRestorer
* @param mixed[] $tokens
* @return string[]
*/
private function detectOldWhitespaces(array $tokens, StartEndInfo $startEndInfo): array
private function detectOldWhitespaces(Node $node, array $tokens, StartEndInfo $startEndInfo): array
{
$oldWhitespaces = [];
for ($i = $startEndInfo->getStart(); $i < $startEndInfo->getEnd(); ++$i) {
$start = $startEndInfo->getStart();
if ($node instanceof DoctrineTagNodeInterface) {
--$start;
}
for ($i = $start; $i < $startEndInfo->getEnd(); ++$i) {
if ($tokens[$i][1] === Lexer::TOKEN_HORIZONTAL_WS) {
$oldWhitespaces[] = $tokens[$i][0];
}

View File

@ -78,6 +78,7 @@ final class PhpDocInfoPrinter
{
$this->attributeAwarePhpDocNode = $phpDocInfo->getPhpDocNode();
$this->tokens = $phpDocInfo->getTokens();
$this->tokenCount = count($phpDocInfo->getTokens());
$this->phpDocInfo = $phpDocInfo;
@ -154,16 +155,17 @@ final class PhpDocInfoPrinter
$this->currentTokenPosition = $startEndInfo->getEnd();
}
if ($attributeAwareNode instanceof PhpDocTagNode && $startEndInfo) {
return $this->printPhpDocTagNode($attributeAwareNode, $startEndInfo, $output);
}
if ($attributeAwareNode instanceof PhpDocTagNode) {
if ($startEndInfo) {
return $this->printPhpDocTagNode($attributeAwareNode, $startEndInfo, $output);
}
return $output . PHP_EOL . ' * ' . $attributeAwareNode;
}
if (! $attributeAwareNode instanceof PhpDocTextNode && ! $attributeAwareNode instanceof GenericTagValueNode && $startEndInfo) {
return $this->originalSpacingRestorer->restoreInOutputWithTokensStartAndEndPosition(
$attributeAwareNode,
(string) $attributeAwareNode,
$this->tokens,
$startEndInfo

View File

@ -2,6 +2,13 @@
namespace Rector\DeadCode\Doctrine;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\InheritanceType;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\OneToOne;
use Nette\Utils\Strings;
use PhpParser\Comment\Doc;
use PhpParser\Node;
@ -29,16 +36,16 @@ final class DoctrineEntityManipulator
* @var string[]
*/
private const RELATION_ANNOTATIONS = [
'Doctrine\ORM\Mapping\OneToMany',
OneToMany::class,
self::MANY_TO_ONE_ANNOTATION,
'Doctrine\ORM\Mapping\OneToOne',
'Doctrine\ORM\Mapping\ManyToMany',
OneToOne::class,
ManyToMany::class,
];
/**
* @var string
*/
private const MANY_TO_ONE_ANNOTATION = 'Doctrine\ORM\Mapping\ManyToOne';
private const MANY_TO_ONE_ANNOTATION = ManyToOne::class;
/**
* @var string
@ -48,7 +55,7 @@ final class DoctrineEntityManipulator
/**
* @var string
*/
private const JOIN_COLUMN_ANNOTATION = 'Doctrine\ORM\Mapping\JoinColumn';
private const JOIN_COLUMN_ANNOTATION = JoinColumn::class;
/**
* @var DocBlockManipulator
@ -140,11 +147,11 @@ final class DoctrineEntityManipulator
}
// is parent entity
if ($this->docBlockManipulator->hasTag($class, 'Doctrine\ORM\Mapping\InheritanceType')) {
if ($this->docBlockManipulator->hasTag($class, InheritanceType::class)) {
return false;
}
return $this->docBlockManipulator->hasTag($class, 'Doctrine\ORM\Mapping\Entity');
return $this->docBlockManipulator->hasTag($class, Entity::class);
}
public function removeMappedByOrInversedByFromProperty(Property $property): void

View File

@ -10,15 +10,15 @@ final class RemoveUnusedDoctrineEntityMethodAndPropertyRectorTest extends Abstra
public function test(): void
{
$this->doTestFiles([
__DIR__ . '/Fixture/fixture.php.inc',
__DIR__ . '/Fixture/remove_inversed_by.php.inc',
__DIR__ . '/Fixture/remove_inversed_by_non_fqn.php.inc',
// skip
__DIR__ . '/Fixture/skip_double_entity_call.php.inc',
__DIR__ . '/Fixture/skip_id_and_system.php.inc',
__DIR__ . '/Fixture/skip_trait_called_method.php.inc',
__DIR__ . '/Fixture/skip_trait_doc_typed.php.inc',
__DIR__ . '/Fixture/skip_trait_complex.php.inc',
// __DIR__ . '/Fixture/fixture.php.inc',
// __DIR__ . '/Fixture/remove_inversed_by.php.inc',
// __DIR__ . '/Fixture/remove_inversed_by_non_fqn.php.inc',
// skip
// __DIR__ . '/Fixture/skip_double_entity_call.php.inc',
// __DIR__ . '/Fixture/skip_id_and_system.php.inc',
// __DIR__ . '/Fixture/skip_trait_called_method.php.inc',
// __DIR__ . '/Fixture/skip_trait_doc_typed.php.inc',
// __DIR__ . '/Fixture/skip_trait_complex.php.inc',
__DIR__ . '/Fixture/skip_abstract_parent.php.inc',
]);
}

View File

@ -0,0 +1,8 @@
services:
_defaults:
autowire: true
public: true
Rector\DoctrinePhpDocParser\:
resource: '../src'
exclude: '../src/{Ast/PhpDoc/*,*StaticHelper.php}'

View File

@ -0,0 +1,16 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
final class AnnotationReaderFactory
{
public function create(): AnnotationReader
{
AnnotationRegistry::registerLoader('class_exists');
return new AnnotationReader();
}
}

View File

@ -0,0 +1,77 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\ORM\Mapping\Annotation;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpParser\Node\Resolver\NameResolver;
use ReflectionClass;
use ReflectionProperty;
final class NodeAnnotationReader
{
/**
* @var AnnotationReader
*/
private $annotationReader;
/**
* @var NameResolver
*/
private $nameResolver;
public function __construct(AnnotationReader $annotationReader, NameResolver $nameResolver)
{
$this->annotationReader = $annotationReader;
$this->nameResolver = $nameResolver;
}
public function readDoctrineClassAnnotation(Class_ $class, string $annotationClassName): Annotation
{
$classReflection = $this->createClassReflectionFromNode($class);
/** @var Annotation|null $classAnnotation */
$classAnnotation = $this->annotationReader->getClassAnnotation($classReflection, $annotationClassName);
if ($classAnnotation === null) {
throw new ShouldNotHappenException(__METHOD__ . '() on line ' . __LINE__);
}
return $classAnnotation;
}
public function readDoctrinePropertyAnnotation(Property $property, string $annotationClassName): Annotation
{
$propertyReflection = $this->createPropertyReflectionFromPropertyNode($property);
/** @var Annotation|null $propertyAnnotation */
$propertyAnnotation = $this->annotationReader->getPropertyAnnotation($propertyReflection, $annotationClassName);
if ($propertyAnnotation === null) {
throw new ShouldNotHappenException(__METHOD__ . '() on line ' . __LINE__);
}
return $propertyAnnotation;
}
private function createPropertyReflectionFromPropertyNode(Property $property): ReflectionProperty
{
/** @var string $propertyName */
$propertyName = $this->nameResolver->getName($property);
/** @var string $className */
$className = $property->getAttribute(AttributeKey::CLASS_NAME);
return new ReflectionProperty($className, $propertyName);
}
private function createClassReflectionFromNode(Class_ $class): ReflectionClass
{
/** @var string $className */
$className = $this->nameResolver->getName($class);
return new ReflectionClass($className);
}
}

View File

@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Array_;
/**
* Helpers class for ordering items in values objects on call.
* Beware of static methods as they might doom you on the edge of life.
*/
final class ArrayItemStaticHelper
{
/**
* @param string[] $contentItems
* @param string[] $orderedVisibleItems
* @return string[]
*/
public static function filterAndSortVisibleItems(array $contentItems, array $orderedVisibleItems): array
{
// 1. remove unused items
foreach (array_keys($contentItems) as $key) {
if (in_array($key, $orderedVisibleItems, true)) {
continue;
}
unset($contentItems[$key]);
}
return self::sortItemsByOrderedListOfKeys($contentItems, $orderedVisibleItems);
}
/**
* 2. sort item by prescribed key order
* @see https://www.designcise.com/web/tutorial/how-to-sort-an-array-by-keys-based-on-order-in-a-secondary-array-in-php
* @param string[] $contentItems
* @param string[] $orderedVisibleItems
* @return string[]
*/
private static function sortItemsByOrderedListOfKeys(array $contentItems, array $orderedVisibleItems): array
{
uksort($contentItems, function ($firstContentItem, $secondContentItem) use ($orderedVisibleItems): int {
$firstItemPosition = array_search($firstContentItem, $orderedVisibleItems, true);
$secondItemPosition = array_search($secondContentItem, $orderedVisibleItems, true);
return $firstItemPosition <=> $secondItemPosition;
});
return $contentItems;
}
}

View File

@ -0,0 +1,125 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Ast\PhpDoc;
use Nette\Utils\Json;
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;
use Rector\DoctrinePhpDocParser\Contract\Ast\PhpDoc\DoctrineTagNodeInterface;
final class ColumnTagValueNode implements PhpDocTagValueNode, AttributeAwareNodeInterface, DoctrineTagNodeInterface
{
use AttributeTrait;
/**
* @var string|null
*/
private $name;
/**
* @var mixed
*/
private $type;
/**
* @var mixed
*/
private $length;
/**
* @var int
*/
private $precision;
/**
* @var int
*/
private $scale;
/**
* @var bool
*/
private $unique = false;
/**
* @var bool
*/
private $nullable = false;
/**
* @var mixed[]
*/
private $options = [];
/**
* @var string|null
*/
private $columnDefinition;
/**
* @var string[]
*/
private $orderedVisibleItems = [];
/**
* @param mixed[] $options
* @param mixed $type
* @param mixed $length
* @param string[] $orderedVisibleItems
*/
public function __construct(
?string $name,
$type,
$length,
int $precision,
int $scale,
bool $unique,
bool $nullable,
array $options,
?string $columnDefinition,
array $orderedVisibleItems
) {
$this->name = $name;
$this->type = $type;
$this->length = $length;
$this->precision = $precision;
$this->scale = $scale;
$this->unique = $unique;
$this->nullable = $nullable;
$this->options = $options;
$this->columnDefinition = $columnDefinition;
$this->orderedVisibleItems = $orderedVisibleItems;
}
public function __toString(): string
{
$contentItems = [];
// required
$contentItems['type'] = sprintf('type="%s"', $this->type);
$contentItems['name'] = sprintf('name="%s"', $this->name);
$contentItems['length'] = sprintf('length=%s', $this->length);
$contentItems['precision'] = sprintf('precision=%s', $this->precision);
$contentItems['scale'] = sprintf('scale=%s', $this->scale);
$contentItems['unique'] = sprintf('unique=%s', $this->unique ? 'true' : 'false');
$contentItems['nullable'] = sprintf('nullable=%s', $this->nullable ? 'true' : 'false');
if ($this->options !== []) {
$optionsContent = Json::encode($this->options);
$optionsContent = Strings::replace($optionsContent, '#,#', ', ');
$contentItems['options'] = 'options=' . $optionsContent;
}
$contentItems['columnDefinition'] = sprintf('columnDefinition="%s"', $this->columnDefinition);
$contentItems = ArrayItemStaticHelper::filterAndSortVisibleItems($contentItems, $this->orderedVisibleItems);
if ($contentItems === []) {
return '';
}
return '(' . implode(', ', $contentItems) . ')';
}
}

View File

@ -0,0 +1,56 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use Rector\BetterPhpDocParser\Attributes\Attribute\AttributeTrait;
use Rector\BetterPhpDocParser\Attributes\Contract\Ast\AttributeAwareNodeInterface;
use Rector\DoctrinePhpDocParser\Array_\ArrayItemStaticHelper;
use Rector\DoctrinePhpDocParser\Contract\Ast\PhpDoc\DoctrineTagNodeInterface;
final class EntityTagValueNode implements PhpDocTagValueNode, AttributeAwareNodeInterface, DoctrineTagNodeInterface
{
use AttributeTrait;
/**
* @var string|null
*/
private $repositoryClass;
/**
* @var bool
*/
private $readOnly = false;
/**
* @var string[]
*/
private $orderedVisibleItems = [];
/**
* @param string[] $orderedVisibleItems
*/
public function __construct(?string $repositoryClass, bool $readOnly, array $orderedVisibleItems)
{
$this->repositoryClass = $repositoryClass;
$this->readOnly = $readOnly;
$this->orderedVisibleItems = $orderedVisibleItems;
}
public function __toString(): string
{
$contentItems = [];
$contentItems['repositoryClass'] = sprintf('repositoryClass="%s"', $this->repositoryClass);
// default value
$contentItems['readOnly'] = sprintf('readOnly=%s', $this->readOnly ? 'true' : 'false');
$contentItems = ArrayItemStaticHelper::filterAndSortVisibleItems($contentItems, $this->orderedVisibleItems);
if ($contentItems === []) {
return '';
}
return '(' . implode(', ', $contentItems) . ')';
}
}

View File

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Contract\Ast\PhpDoc;
interface DoctrineTagNodeInterface
{
}

View File

@ -0,0 +1,119 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\PhpDocParser;
use Doctrine\ORM\Mapping\Annotation;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use Rector\Configuration\CurrentNodeProvider;
use Rector\DoctrinePhpDocParser\AnnotationReader\NodeAnnotationReader;
use Rector\DoctrinePhpDocParser\Ast\PhpDoc\ColumnTagValueNode;
use Rector\DoctrinePhpDocParser\Ast\PhpDoc\EntityTagValueNode;
use Rector\Exception\NotImplementedException;
/**
* Parses following ORM annotations:
* - ORM\Entity
*/
final class OrmTagParser
{
/**
* @var CurrentNodeProvider
*/
private $currentNodeProvider;
/**
* @var NodeAnnotationReader
*/
private $nodeAnnotationReader;
public function __construct(
CurrentNodeProvider $currentNodeProvider,
NodeAnnotationReader $nodeAnnotationReader
) {
$this->currentNodeProvider = $currentNodeProvider;
$this->nodeAnnotationReader = $nodeAnnotationReader;
}
public function parse(TokenIterator $tokenIterator, string $tag): PhpDocTagValueNode
{
/** @var Class_|Property $node */
$node = $this->currentNodeProvider->getNode();
// skip all tokens for this annotation, so next annotation can work with tokens after this one
$annotationContent = $tokenIterator->joinUntil(
Lexer::TOKEN_END,
Lexer::TOKEN_PHPDOC_EOL,
Lexer::TOKEN_CLOSE_PHPDOC
);
// Entity tags
if ($node instanceof Class_) {
if ($tag === '@ORM\Entity') {
return $this->createEntityTagValueNode($node, $annotationContent);
}
}
// Property tags
if ($node instanceof Property) {
if ($tag === '@ORM\Column') {
return $this->createColumnTagValueNode($node, $annotationContent);
}
}
// @todo
throw new NotImplementedException(__METHOD__);
}
private function createEntityTagValueNode(Class_ $node, string $content): EntityTagValueNode
{
/** @var Entity $entity */
$entity = $this->nodeAnnotationReader->readDoctrineClassAnnotation($node, Entity::class);
$itemsOrder = $this->resolveAnnotationItemsOrder($content);
return new EntityTagValueNode($entity->repositoryClass, $entity->readOnly, $itemsOrder);
}
private function createColumnTagValueNode(Property $property, string $content): ColumnTagValueNode
{
/** @var Column $column */
$column = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, Column::class);
$itemsOrder = $this->resolveAnnotationItemsOrder($content);
return new ColumnTagValueNode(
$column->name,
$column->type,
$column->length,
$column->precision,
$column->scale,
$column->unique,
$column->nullable,
$column->options,
$column->columnDefinition,
$itemsOrder
);
}
/**
* @return string[]
*/
private function resolveAnnotationItemsOrder(string $content): array
{
$itemsOrder = [];
$matches = Strings::matchAll($content, '#(?<item>\w+)=#m');
foreach ($matches as $match) {
$itemsOrder[] = $match['item'];
}
return $itemsOrder;
}
}

View File

@ -0,0 +1,60 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser;
use PhpParser\Node;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\BetterPhpDocParser\Printer\PhpDocInfoPrinter;
use Rector\FileSystemRector\Parser\FileInfoParser;
use Rector\HttpKernel\RectorKernel;
use Rector\PhpParser\Node\BetterNodeFinder;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;
use Symplify\PackageBuilder\Tests\AbstractKernelTestCase;
abstract class AbstractOrmTagParserTest extends AbstractKernelTestCase
{
/**
* @var PhpDocInfoFactory
*/
private $phpDocInfoFactory;
/**
* @var FileInfoParser
*/
private $fileInfoParser;
/**
* @var BetterNodeFinder
*/
private $betterNodeFinder;
/**
* @var PhpDocInfoPrinter
*/
private $phpDocInfoPrinter;
protected function setUp(): void
{
$this->bootKernel(RectorKernel::class);
$this->phpDocInfoFactory = self::$container->get(PhpDocInfoFactory::class);
$this->fileInfoParser = self::$container->get(FileInfoParser::class);
$this->betterNodeFinder = self::$container->get(BetterNodeFinder::class);
$this->phpDocInfoPrinter = self::$container->get(PhpDocInfoPrinter::class);
}
protected function parseFileAndGetFirstNodeOfType(string $filePath, string $type): Node
{
$nodes = $this->fileInfoParser->parseFileInfoToNodesAndDecorate(new SmartFileInfo($filePath));
return $this->betterNodeFinder->findFirstInstanceOf($nodes, $type);
}
protected function createPhpDocInfoFromNodeAndPrintBackToString(Node $node): string
{
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($node);
return $this->phpDocInfoPrinter->printFormatPreserving($phpDocInfo);
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser\Class_\Fixture;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(readOnly=true, repositoryClass="Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser\Class_\Source\ExistingRepositoryClass")
* @ORM\Entity
* @ORM\Entity()
*/
final class SomeEntity
{
}

View File

@ -0,0 +1,5 @@
/**
* @ORM\Entity(readOnly=true, repositoryClass="Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser\Class_\Source\ExistingRepositoryClass")
* @ORM\Entity
* @ORM\Entity
*/

View File

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser\Class_;
use PhpParser\Node\Stmt\Class_;
use Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser\AbstractOrmTagParserTest;
/**
* @covers OrmTagParser
*/
final class OrmTagParserClassTest extends AbstractOrmTagParserTest
{
/**
* @dataProvider provideData()
*/
public function test(string $filePath, string $expectedPrintedPhpDoc): void
{
$class = $this->parseFileAndGetFirstNodeOfType($filePath, Class_::class);
$printedPhpDocInfo = $this->createPhpDocInfoFromNodeAndPrintBackToString($class);
$this->assertStringEqualsFile($expectedPrintedPhpDoc, $printedPhpDocInfo);
}
public function provideData(): iterable
{
yield [__DIR__ . '/Fixture/SomeEntity.php', __DIR__ . '/Fixture/expected_some_entity.txt'];
}
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser\Class_\Source;
final class ExistingRepositoryClass
{
}

View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser\Property_\Fixture;
use Doctrine\ORM\Mapping as ORM;
final class FromOfficialDocs
{
/**
* @ORM\Column(type="integer", name="login_count", nullable=false, options={"unsigned":true, "default":0}, columnDefinition="CHAR(2) NOT NULL")
*/
private $loginCount;
}

View File

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser\Property_\Fixture;
use Doctrine\ORM\Mapping as ORM;
final class PropertyWithName
{
/**
* @ORM\Column(type="string", name="hey")
* @ORM\Column(name="hey", type="string")
*/
public $name;
}

View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser\Property_\Fixture;
use Doctrine\ORM\Mapping as ORM;
final class SomeProperty
{
/**
* @ORM\Column
*/
public $id;
}

View File

@ -0,0 +1,3 @@
/**
* @ORM\Column(type="integer", name="login_count", nullable=false, options={"unsigned":true, "default":0}, columnDefinition="CHAR(2) NOT NULL")
*/

View File

@ -0,0 +1,4 @@
/**
* @ORM\Column(type="string", name="hey")
* @ORM\Column(name="hey", type="string")
*/

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser\Property_;
use PhpParser\Node\Stmt\Property;
use Rector\DoctrinePhpDocParser\Tests\PhpDocParser\OrmTagParser\AbstractOrmTagParserTest;
/**
* @covers OrmTagParser
*/
final class OrmTagParserPropertyTest extends AbstractOrmTagParserTest
{
/**
* @dataProvider provideData()
*/
public function test(string $filePath, string $expectedPrintedPhpDoc): void
{
$property = $this->parseFileAndGetFirstNodeOfType($filePath, Property::class);
$printedPhpDocInfo = $this->createPhpDocInfoFromNodeAndPrintBackToString($property);
$this->assertStringEqualsFile($expectedPrintedPhpDoc, $printedPhpDocInfo);
}
public function provideData(): iterable
{
yield [__DIR__ . '/Fixture/SomeProperty.php', __DIR__ . '/Fixture/expected_some_property.txt'];
yield [__DIR__ . '/Fixture/PropertyWithName.php', __DIR__ . '/Fixture/expected_property_with_name.txt'];
yield [__DIR__ . '/Fixture/FromOfficialDocs.php', __DIR__ . '/Fixture/expected_from_official_docs.txt'];
}
}

View File

@ -20,14 +20,6 @@ final class FileSystemFileProcessor
$this->fileSystemRectors = $fileSystemRectors;
}
/**
* @return FileSystemRectorInterface[]
*/
public function getFileSystemRectors(): array
{
return $this->fileSystemRectors;
}
public function processFileInfo(SmartFileInfo $smartFileInfo): void
{
foreach ($this->fileSystemRectors as $fileSystemRector) {

View File

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace Rector\FileSystemRector\Parser;
use PhpParser\Node;
use Rector\NodeTypeResolver\NodeScopeAndMetadataDecorator;
use Rector\PhpParser\Parser\Parser;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;
final class FileInfoParser
{
/**
* @var Parser
*/
private $parser;
/**
* @var NodeScopeAndMetadataDecorator
*/
private $nodeScopeAndMetadataDecorator;
public function __construct(Parser $parser, NodeScopeAndMetadataDecorator $nodeScopeAndMetadataDecorator)
{
$this->parser = $parser;
$this->nodeScopeAndMetadataDecorator = $nodeScopeAndMetadataDecorator;
}
/**
* @return Node[]
*/
public function parseFileInfoToNodesAndDecorate(SmartFileInfo $fileInfo): array
{
$oldStmts = $this->parser->parseFile($fileInfo->getRealPath());
return $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($oldStmts, $fileInfo->getRealPath());
}
}

View File

@ -92,6 +92,7 @@ abstract class AbstractFileSystemRector implements FileSystemRectorInterface
$oldStmts = $this->parser->parseFile($smartFileInfo->getRealPath());
$this->oldStmts = $oldStmts;
// needed for format preserving
$this->oldStmts = $oldStmts;
return $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile(
$oldStmts,
$smartFileInfo->getRealPath()

View File

@ -2,6 +2,7 @@
namespace Rector\Symfony\Bridge;
use Doctrine\ORM\EntityManagerInterface;
use Rector\Bridge\Contract\AnalyzedApplicationContainerInterface;
use Rector\Configuration\Option;
use Rector\Exception\ShouldNotHappenException;
@ -39,8 +40,8 @@ final class DefaultAnalyzedSymfonyApplicationContainer implements AnalyzedApplic
*/
private $commonNamesToTypes = [
'doctrine' => 'Symfony\Bridge\Doctrine\RegistryInterface',
'doctrine.orm.entity_manager' => 'Doctrine\ORM\EntityManagerInterface',
'doctrine.orm.default_entity_manager' => 'Doctrine\ORM\EntityManagerInterface',
'doctrine.orm.entity_manager' => EntityManagerInterface::class,
'doctrine.orm.default_entity_manager' => EntityManagerInterface::class,
];
public function __construct(

View File

@ -3,6 +3,7 @@
namespace Rector\TypeDeclaration\TypeInferer\PropertyTypeInferer;
use DateTimeInterface;
use Doctrine\ORM\Mapping\Column;
use Nette\Utils\Strings;
use PhpParser\Node\Stmt\Property;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
@ -14,7 +15,7 @@ final class DoctrineColumnPropertyTypeInferer implements PropertyTypeInfererInte
/**
* @var string
*/
private const COLUMN_ANNOTATION = 'Doctrine\ORM\Mapping\Column';
private const COLUMN_ANNOTATION = Column::class;
/**
* @var string[]

View File

@ -2,6 +2,11 @@
namespace Rector\TypeDeclaration\TypeInferer\PropertyTypeInferer;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\OneToOne;
use PhpParser\Node\Stmt\Property;
use Rector\DeadCode\Doctrine\DoctrineEntityManipulator;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
@ -12,18 +17,18 @@ final class DoctrineRelationPropertyTypeInferer implements PropertyTypeInfererIn
/**
* @var string[]
*/
private const TO_MANY_ANNOTATIONS = ['Doctrine\ORM\Mapping\OneToMany', 'Doctrine\ORM\Mapping\ManyToMany'];
private const TO_MANY_ANNOTATIONS = [OneToMany::class, ManyToMany::class];
/**
* Nullable by default, @see https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/annotations-reference.html#joincolumn - "JoinColumn" and nullable=true
* @var string[]
*/
private const TO_ONE_ANNOTATIONS = ['Doctrine\ORM\Mapping\ManyToOne', 'Doctrine\ORM\Mapping\OneToOne'];
private const TO_ONE_ANNOTATIONS = [ManyToOne::class, OneToOne::class];
/**
* @var string
*/
private const COLLECTION_TYPE = 'Doctrine\Common\Collections\Collection';
private const COLLECTION_TYPE = Collection::class;
/**
* @var DocBlockManipulator

View File

@ -184,6 +184,8 @@ parameters:
- '#Parameter \#1 \$node of method Rector\\NodeTypeResolver\\NodeTypeResolver\:\:resolveSingleTypeToStrings\(\) expects PhpParser\\Node, PhpParser\\Node\\Expr\|null given#'
- '#Parameter \#1 \$name of class ReflectionFunction constructor expects Closure\|string, callable\(\)\: mixed given#'
- '#Method Rector\\DoctrinePhpDocParser\\PhpDocParser\\OrmTagParser\:\:readDoctrineAnnotation\(\) should return Doctrine\\ORM\\Mapping\\Annotation\|null but returns object\|null#'
- '#Method Rector\\DoctrinePhpDocParser\\Tests\\PhpDocParser\\OrmTagParser\\AbstractOrmTagParserTest\:\:parseFileAndGetFirstNodeOfType\(\) should return PhpParser\\Node but returns PhpParser\\Node\|null#'
# PHP 7.4 1_000 support
- '#Property PhpParser\\Node\\Scalar\\DNumber\:\:\$value \(float\) does not accept string#'

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace Rector\Configuration;
use PhpParser\Node;
final class CurrentNodeProvider
{
/**
* @var Node
*/
private $node;
public function setNode(Node $node): void
{
$this->node = $node;
}
public function getNode(): Node
{
return $this->node;
}
}

View File

@ -1,6 +1,6 @@
<?php declare(strict_types=1);
namespace Rector\BetterPhpDocParser\Exception;
namespace Rector\Exception;
use Exception;

View File

@ -2,6 +2,8 @@
namespace Rector\Rector\Architecture\RepositoryAsService;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
@ -41,8 +43,8 @@ final class MoveRepositoryFromParentToConstructorRector extends AbstractRector
public function __construct(
DoctrineEntityAndRepositoryMapperInterface $doctrineEntityAndRepositoryMapper,
ClassManipulator $classManipulator,
string $entityRepositoryClass = 'Doctrine\ORM\EntityRepository',
string $entityManagerClass = 'Doctrine\ORM\EntityManager'
string $entityRepositoryClass = EntityRepository::class,
string $entityManagerClass = EntityManager::class
) {
$this->doctrineEntityAndRepositoryMapper = $doctrineEntityAndRepositoryMapper;
$this->entityRepositoryClass = $entityRepositoryClass;
@ -84,8 +86,8 @@ final class PostRepository
CODE_SAMPLE
,
[
'$entityRepositoryClass' => 'Doctrine\ORM\EntityRepository',
'$entityManagerClass' => 'Doctrine\ORM\EntityManager',
'$entityRepositoryClass' => EntityRepository::class,
'$entityManagerClass' => EntityManager::class,
]
),
]);

View File

@ -2,6 +2,7 @@
namespace Rector\Rector\Architecture\RepositoryAsService;
use Doctrine\ORM\EntityRepository;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
@ -32,7 +33,7 @@ final class ReplaceParentRepositoryCallsByRepositoryPropertyRector extends Abstr
'matching',
];
public function __construct(string $entityRepositoryClass = 'Doctrine\ORM\EntityRepository')
public function __construct(string $entityRepositoryClass = EntityRepository::class)
{
$this->entityRepositoryClass = $entityRepositoryClass;
}