diff --git a/config/sets/typed-collections.php b/config/sets/typed-collections.php index 63663a72..8b3b9fed 100644 --- a/config/sets/typed-collections.php +++ b/config/sets/typed-collections.php @@ -6,10 +6,12 @@ use Rector\Doctrine\CodeQuality\Rector\Class_\AddReturnDocBlockToCollectionPropertyGetterByToManyAnnotationRector; use Rector\Doctrine\CodeQuality\Rector\Class_\ExplicitRelationCollectionRector; use Rector\Doctrine\CodeQuality\Rector\Property\ImproveDoctrineCollectionDocTypeInEntityRector; +use Rector\Doctrine\CodeQuality\Rector\Property\TypedPropertyFromToManyRelationTypeRector; return RectorConfig::configure() ->withRules([ ExplicitRelationCollectionRector::class, AddReturnDocBlockToCollectionPropertyGetterByToManyAnnotationRector::class, + TypedPropertyFromToManyRelationTypeRector::class, ImproveDoctrineCollectionDocTypeInEntityRector::class, ]); diff --git a/rules-tests/CodeQuality/Rector/Class_/AddReturnDocBlockToCollectionPropertyGetterByToManyAnnotationRector/Fixture/ManyToMany/attribute_getter_with_return_type.php b/rules-tests/CodeQuality/Rector/Class_/AddReturnDocBlockToCollectionPropertyGetterByToManyAnnotationRector/Fixture/ManyToMany/attribute_getter_with_return_type.php deleted file mode 100644 index 886d6c75..00000000 --- a/rules-tests/CodeQuality/Rector/Class_/AddReturnDocBlockToCollectionPropertyGetterByToManyAnnotationRector/Fixture/ManyToMany/attribute_getter_with_return_type.php +++ /dev/null @@ -1,21 +0,0 @@ -trainings; - } -} - -?> diff --git a/rules-tests/CodeQuality/Rector/Property/TypedPropertyFromToManyRelationTypeRector/Fixture/odm_to_many.php.inc b/rules-tests/CodeQuality/Rector/Property/TypedPropertyFromToManyRelationTypeRector/Fixture/odm_to_many.php.inc new file mode 100644 index 00000000..48c711af --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Property/TypedPropertyFromToManyRelationTypeRector/Fixture/odm_to_many.php.inc @@ -0,0 +1,50 @@ +products = new ArrayCollection(); + } +} + +?> +----- + + */ + private \Doctrine\Common\Collections\Collection $products; + + public function __construct() + { + $this->products = new ArrayCollection(); + } +} + +?> diff --git a/rules/CodeQuality/Enum/ToManyMappings.php b/rules/CodeQuality/Enum/CollectionMapping.php similarity index 62% rename from rules/CodeQuality/Enum/ToManyMappings.php rename to rules/CodeQuality/Enum/CollectionMapping.php index acc462c2..14406077 100644 --- a/rules/CodeQuality/Enum/ToManyMappings.php +++ b/rules/CodeQuality/Enum/CollectionMapping.php @@ -7,14 +7,19 @@ use Rector\Doctrine\Enum\MappingClass; use Rector\Doctrine\Enum\OdmMappingClass; -class ToManyMappings +final class CollectionMapping { /** * @var string[] */ - final public const TO_MANY_CLASSES = [ + public const TO_MANY_CLASSES = [ MappingClass::ONE_TO_MANY, MappingClass::MANY_TO_MANY, OdmMappingClass::REFERENCE_MANY, ]; + + /** + * @var string[] + */ + public const TO_ONE_CLASSES = [MappingClass::MANY_TO_ONE, MappingClass::ONE_TO_ONE]; } diff --git a/rules/CodeQuality/Enum/OdmMappingKey.php b/rules/CodeQuality/Enum/OdmMappingKey.php new file mode 100644 index 00000000..5da8f5da --- /dev/null +++ b/rules/CodeQuality/Enum/OdmMappingKey.php @@ -0,0 +1,16 @@ +phpDocInfoFactory->createFromNodeOrEmpty($property); - if ($phpDocInfo->hasByAnnotationClasses(ToManyMappings::TO_MANY_CLASSES)) { + if ($phpDocInfo->hasByAnnotationClasses(CollectionMapping::TO_MANY_CLASSES)) { return $this->refactorPropertyPhpDocInfo($property, $phpDocInfo); } $targetEntityExpr = $this->attributeFinder->findAttributeByClassesArgByName( $property, - ToManyMappings::TO_MANY_CLASSES, + CollectionMapping::TO_MANY_CLASSES, 'targetEntity' ); if (! $targetEntityExpr instanceof Expr) { diff --git a/rules/CodeQuality/Rector/Property/TypedPropertyFromToManyRelationTypeRector.php b/rules/CodeQuality/Rector/Property/TypedPropertyFromToManyRelationTypeRector.php index a13c8196..d4fe234a 100644 --- a/rules/CodeQuality/Rector/Property/TypedPropertyFromToManyRelationTypeRector.php +++ b/rules/CodeQuality/Rector/Property/TypedPropertyFromToManyRelationTypeRector.php @@ -92,7 +92,6 @@ public function refactor(Node $node): Property|null } $propertyType = $this->toManyRelationPropertyTypeResolver->resolve($node); - if (! $propertyType instanceof Type || $propertyType instanceof MixedType) { return null; } diff --git a/src/Enum/MappingClass.php b/src/Enum/MappingClass.php index ecc29d21..b78724d4 100644 --- a/src/Enum/MappingClass.php +++ b/src/Enum/MappingClass.php @@ -106,6 +106,11 @@ final class MappingClass */ public const ONE_TO_MANY = 'Doctrine\ORM\Mapping\OneToMany'; + /** + * @var string + */ + public const ONE_TO_ONE = 'Doctrine\ORM\Mapping\OneToOne'; + /** * @var string */ diff --git a/src/NodeAnalyzer/AttributeFinder.php b/src/NodeAnalyzer/AttributeFinder.php index 0f0d4779..4d6c9aec 100644 --- a/src/NodeAnalyzer/AttributeFinder.php +++ b/src/NodeAnalyzer/AttributeFinder.php @@ -37,6 +37,30 @@ public function findAttributeByClassArgByName( return $this->findAttributeByClassesArgByName($node, [$attributeClass], $argName); } + /** + * @param string[] $attributeClasses + * @param string[] $argNames + */ + public function findAttributeByClassesArgByNames( + ClassMethod | Property | ClassLike | Param $node, + array $attributeClasses, + array $argNames + ): ?Expr { + $attribute = $this->findAttributeByClasses($node, $attributeClasses); + if (! $attribute instanceof Attribute) { + return null; + } + + foreach ($argNames as $argName) { + $argExpr = $this->findArgByName($attribute, $argName); + if ($argExpr instanceof Expr) { + return $argExpr; + } + } + + return null; + } + /** * @param string[] $attributeClasses */ diff --git a/src/NodeManipulator/ToManyRelationPropertyTypeResolver.php b/src/NodeManipulator/ToManyRelationPropertyTypeResolver.php index 6d0a45bb..3e2bbcf3 100644 --- a/src/NodeManipulator/ToManyRelationPropertyTypeResolver.php +++ b/src/NodeManipulator/ToManyRelationPropertyTypeResolver.php @@ -12,6 +12,9 @@ use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode; use Rector\BetterPhpDocParser\PhpDoc\StringNode; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; +use Rector\Doctrine\CodeQuality\Enum\CollectionMapping; +use Rector\Doctrine\CodeQuality\Enum\EntityMappingKey; +use Rector\Doctrine\CodeQuality\Enum\OdmMappingKey; use Rector\Doctrine\NodeAnalyzer\AttributeFinder; use Rector\Doctrine\PhpDoc\ShortClassExpander; use Rector\Doctrine\TypeAnalyzer\CollectionTypeFactory; @@ -25,14 +28,6 @@ */ private const COLLECTION_TYPE = 'Doctrine\Common\Collections\Collection'; - /** - * @var class-string[] - */ - private const TO_MANY_ANNOTATION_CLASSES = [ - 'Doctrine\ORM\Mapping\OneToMany', - 'Doctrine\ORM\Mapping\ManyToMany', - ]; - public function __construct( private PhpDocInfoFactory $phpDocInfoFactory, private ShortClassExpander $shortClassExpander, @@ -46,15 +41,15 @@ public function resolve(Property $property): ?Type { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); - $doctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClasses(self::TO_MANY_ANNOTATION_CLASSES); + $doctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClasses(CollectionMapping::TO_MANY_CLASSES); if ($doctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) { return $this->processToManyRelation($property, $doctrineAnnotationTagValueNode); } - $expr = $this->attributeFinder->findAttributeByClassesArgByName( + $expr = $this->attributeFinder->findAttributeByClassesArgByNames( $property, - self::TO_MANY_ANNOTATION_CLASSES, - 'targetEntity' + CollectionMapping::TO_MANY_CLASSES, + [EntityMappingKey::TARGET_ENTITY, OdmMappingKey::TARGET_DOCUMENT] ); if (! $expr instanceof Expr) { @@ -68,7 +63,9 @@ private function processToManyRelation( Property $property, DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode ): Type|null { - $targetEntityArrayItemNode = $doctrineAnnotationTagValueNode->getValue('targetEntity'); + $targetEntityArrayItemNode = $doctrineAnnotationTagValueNode->getValue( + EntityMappingKey::TARGET_ENTITY + ) ?: $doctrineAnnotationTagValueNode->getValue(OdmMappingKey::TARGET_DOCUMENT); if (! $targetEntityArrayItemNode instanceof ArrayItemNode) { return null; } diff --git a/src/NodeManipulator/ToOneRelationPropertyTypeResolver.php b/src/NodeManipulator/ToOneRelationPropertyTypeResolver.php index d19fdcd3..ffcfd1e7 100644 --- a/src/NodeManipulator/ToOneRelationPropertyTypeResolver.php +++ b/src/NodeManipulator/ToOneRelationPropertyTypeResolver.php @@ -18,6 +18,8 @@ use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; use Rector\BetterPhpDocParser\PhpDocParser\ClassAnnotationMatcher; +use Rector\Doctrine\CodeQuality\Enum\CollectionMapping; +use Rector\Doctrine\CodeQuality\Enum\EntityMappingKey; use Rector\Doctrine\NodeAnalyzer\AttributeFinder; use Rector\Doctrine\NodeAnalyzer\TargetEntityResolver; use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory; @@ -25,11 +27,6 @@ final readonly class ToOneRelationPropertyTypeResolver { - /** - * @var class-string[] - */ - private const TO_ONE_ANNOTATION_CLASSES = ['Doctrine\ORM\Mapping\ManyToOne', 'Doctrine\ORM\Mapping\OneToOne']; - private const JOIN_COLUMN = ['Doctrine\ORM\Mapping\JoinColumn', 'Doctrine\ORM\Mapping\Column']; public function __construct( @@ -45,7 +42,7 @@ public function resolve(Property $property, bool $forceNullable): ?Type { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); - $doctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClasses(self::TO_ONE_ANNOTATION_CLASSES); + $doctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClasses(CollectionMapping::TO_ONE_CLASSES); if ($doctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) { return $this->resolveFromDocBlock($phpDocInfo, $property, $doctrineAnnotationTagValueNode, $forceNullable); @@ -53,8 +50,8 @@ public function resolve(Property $property, bool $forceNullable): ?Type $expr = $this->attributeFinder->findAttributeByClassesArgByName( $property, - self::TO_ONE_ANNOTATION_CLASSES, - 'targetEntity' + CollectionMapping::TO_ONE_CLASSES, + EntityMappingKey::TARGET_ENTITY ); if (! $expr instanceof Expr) { @@ -78,7 +75,7 @@ private function processToOneRelation( ?DoctrineAnnotationTagValueNode $joinDoctrineAnnotationTagValueNode, bool $forceNullable ): Type { - $targetEntityArrayItemNode = $toOneDoctrineAnnotationTagValueNode->getValue('targetEntity'); + $targetEntityArrayItemNode = $toOneDoctrineAnnotationTagValueNode->getValue(EntityMappingKey::TARGET_ENTITY); if (! $targetEntityArrayItemNode instanceof ArrayItemNode) { return new MixedType(); } diff --git a/src/TypeAnalyzer/CollectionTypeResolver.php b/src/TypeAnalyzer/CollectionTypeResolver.php index c200a25b..f6ef199f 100644 --- a/src/TypeAnalyzer/CollectionTypeResolver.php +++ b/src/TypeAnalyzer/CollectionTypeResolver.php @@ -18,7 +18,9 @@ use Rector\BetterPhpDocParser\PhpDoc\ArrayItemNode; use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode; use Rector\BetterPhpDocParser\PhpDoc\StringNode; -use Rector\Doctrine\CodeQuality\Enum\ToManyMappings; +use Rector\Doctrine\CodeQuality\Enum\CollectionMapping; +use Rector\Doctrine\CodeQuality\Enum\EntityMappingKey; +use Rector\Doctrine\CodeQuality\Enum\OdmMappingKey; use Rector\Doctrine\NodeAnalyzer\AttrinationFinder; use Rector\Doctrine\NodeAnalyzer\TargetEntityResolver; use Rector\Doctrine\PhpDoc\ShortClassExpander; @@ -27,11 +29,6 @@ final readonly class CollectionTypeResolver { - /** - * @var string - */ - private const TARGET_ENTITY = 'targetEntity'; - /** * @var string */ @@ -69,7 +66,7 @@ public function resolveFromToManyProperty(Property $property): ?FullyQualifiedOb { $doctrineAnnotationTagValueNodeOrAttribute = $this->attrinationFinder->getByMany( $property, - ToManyMappings::TO_MANY_CLASSES + CollectionMapping::TO_MANY_CLASSES ); if ($doctrineAnnotationTagValueNodeOrAttribute instanceof DoctrineAnnotationTagValueNode) { @@ -82,7 +79,7 @@ public function resolveFromToManyProperty(Property $property): ?FullyQualifiedOb if ($doctrineAnnotationTagValueNodeOrAttribute instanceof Attribute) { $targetEntityExpr = $this->findExprByArgNames( $doctrineAnnotationTagValueNodeOrAttribute->args, - [self::TARGET_ENTITY, self::TARGET_DOCUMENT] + [EntityMappingKey::TARGET_ENTITY, OdmMappingKey::TARGET_DOCUMENT] ); if (! $targetEntityExpr instanceof ClassConstFetch) { @@ -104,7 +101,7 @@ private function resolveFromDoctrineAnnotationTagValueNode( DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode, Property $property ): ?FullyQualifiedObjectType { - $targetEntityArrayItemNode = $doctrineAnnotationTagValueNode->getValue(self::TARGET_ENTITY); + $targetEntityArrayItemNode = $doctrineAnnotationTagValueNode->getValue(EntityMappingKey::TARGET_ENTITY); // in case of ODM $targetDocumentArrayItemNode = $doctrineAnnotationTagValueNode->getValue(self::TARGET_DOCUMENT); diff --git a/src/TypeAnalyzer/CollectionVarTagValueNodeResolver.php b/src/TypeAnalyzer/CollectionVarTagValueNodeResolver.php index 1862cc49..5fc21443 100644 --- a/src/TypeAnalyzer/CollectionVarTagValueNodeResolver.php +++ b/src/TypeAnalyzer/CollectionVarTagValueNodeResolver.php @@ -7,7 +7,7 @@ use PhpParser\Node\Stmt\Property; use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; -use Rector\Doctrine\CodeQuality\Enum\ToManyMappings; +use Rector\Doctrine\CodeQuality\Enum\CollectionMapping; final readonly class CollectionVarTagValueNodeResolver { @@ -19,7 +19,7 @@ public function __construct( public function resolve(Property $property): ?VarTagValueNode { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); - if (! $phpDocInfo->hasByAnnotationClasses(ToManyMappings::TO_MANY_CLASSES)) { + if (! $phpDocInfo->hasByAnnotationClasses(CollectionMapping::TO_MANY_CLASSES)) { return null; }