diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 53ef78e5b13..ee45f8408f0 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -83,7 +83,7 @@ protected function prepare(): void } // Mark any non-collection opposite sides as fetched, too. - if ($assoc['mappedBy']) { + if (! $assoc->isOwningSide()) { $this->hints['fetched'][$dqlAlias][$assoc['mappedBy']] = true; continue; @@ -439,7 +439,7 @@ protected function hydrateRowData(array $row, array &$result): void $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($element, $parentObject); $this->uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc['fieldName'], $parentObject); } - } elseif ($parentClass === $targetClass && $relation['mappedBy']) { + } elseif ($parentClass === $targetClass && ! $relation->isOwningSide()) { // Special case: bi-directional self-referencing one-one on the same class $targetClass->reflFields[$relationField]->setValue($element, $parentObject); } diff --git a/lib/Doctrine/ORM/Mapping/AssociationMapping.php b/lib/Doctrine/ORM/Mapping/AssociationMapping.php index af0a48e993f..0e9ea2282e4 100644 --- a/lib/Doctrine/ORM/Mapping/AssociationMapping.php +++ b/lib/Doctrine/ORM/Mapping/AssociationMapping.php @@ -17,22 +17,6 @@ /** @template-implements ArrayAccess */ abstract class AssociationMapping implements ArrayAccess { - /** - * required for bidirectional associations - * The name of the field that completes the bidirectional association on - * the owning side. This key must be specified on the inverse side of a - * bidirectional association. - */ - public string|null $mappedBy = null; - - /** - * required for bidirectional associations - * The name of the field that completes the bidirectional association on - * the inverse side. This key must be specified on the owning side of a - * bidirectional association. - */ - public string|null $inversedBy = null; - /** * The names of persistence operations to cascade on the association. * @@ -159,12 +143,12 @@ public static function fromMappingArray(array $mappingArray): static } /** - * @psalm-assert-if-true AssociationOwningSideMapping $this - * @psalm-assert-if-false string $this->mappedBy + * @psalm-assert-if-true OwningSideMapping $this + * @psalm-assert-if-false InverseSideMapping $this */ final public function isOwningSide(): bool { - return $this instanceof AssociationOwningSideMapping; + return $this instanceof OwningSideMapping; } /** @psalm-assert-if-true ToOneAssociationMapping $this */ @@ -335,8 +319,6 @@ public function __sleep(): array foreach ( [ - 'mappedBy', - 'inversedBy', 'fetch', 'inherited', 'declared', diff --git a/lib/Doctrine/ORM/Mapping/AssociationOwningSideMapping.php b/lib/Doctrine/ORM/Mapping/AssociationOwningSideMapping.php deleted file mode 100644 index 6230e72dedf..00000000000 --- a/lib/Doctrine/ORM/Mapping/AssociationOwningSideMapping.php +++ /dev/null @@ -1,9 +0,0 @@ -table ?? null, $this->isInheritanceTypeSingleTable(), ) : - OneToOneAssociationMapping::fromMappingArrayAndName( - $mapping, - $this->namingStrategy, - $this->name, - $this->table, - $this->isInheritanceTypeSingleTable(), - ); + OneToOneInverseSideMapping::fromMappingArrayAndName($mapping, $this->name); case self::MANY_TO_ONE: return ManyToOneAssociationMapping::fromMappingArrayAndName( @@ -1419,7 +1413,7 @@ protected function _validateAndCompleteAssociationMapping(array $mapping): Assoc return $mapping['isOwningSide'] ? ManyToManyOwningSideMapping::fromMappingArrayAndNamingStrategy($mapping, $this->namingStrategy) : - ManyToManyAssociationMapping::fromMappingArray($mapping); + ManyToManyInverseSideMapping::fromMappingArray($mapping); default: throw MappingException::invalidAssociationType( diff --git a/lib/Doctrine/ORM/Mapping/InverseSideMapping.php b/lib/Doctrine/ORM/Mapping/InverseSideMapping.php new file mode 100644 index 00000000000..56dce9f27ec --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/InverseSideMapping.php @@ -0,0 +1,30 @@ +mappedBy; + } + + /** @return list */ + public function __sleep(): array + { + return [ + ...parent::__sleep(), + 'mappedBy', + ]; + } +} diff --git a/lib/Doctrine/ORM/Mapping/ManyToManyAssociationMapping.php b/lib/Doctrine/ORM/Mapping/ManyToManyAssociationMapping.php index 92501efd575..8d963c29469 100644 --- a/lib/Doctrine/ORM/Mapping/ManyToManyAssociationMapping.php +++ b/lib/Doctrine/ORM/Mapping/ManyToManyAssociationMapping.php @@ -4,6 +4,6 @@ namespace Doctrine\ORM\Mapping; -class ManyToManyAssociationMapping extends ToManyAssociationMapping +interface ManyToManyAssociationMapping extends ToManyAssociationMapping { } diff --git a/lib/Doctrine/ORM/Mapping/ManyToManyInverseSideMapping.php b/lib/Doctrine/ORM/Mapping/ManyToManyInverseSideMapping.php new file mode 100644 index 00000000000..8cd5cbddb3e --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/ManyToManyInverseSideMapping.php @@ -0,0 +1,9 @@ + */ - public array $sourceToTargetKeyColumns = []; - - /** @var array */ - public array $targetToSourceKeyColumns = []; - - /** @var list */ - public array $joinColumns = []; - - /** @return list */ - public function __sleep(): array - { - $serialized = parent::__sleep(); - - $serialized[] = 'joinColumns'; - $serialized[] = 'sourceToTargetKeyColumns'; - $serialized[] = 'targetToSourceKeyColumns'; - - return $serialized; - } } diff --git a/lib/Doctrine/ORM/Mapping/OneToManyAssociationMapping.php b/lib/Doctrine/ORM/Mapping/OneToManyAssociationMapping.php index 5849a3382b7..804061a1a1d 100644 --- a/lib/Doctrine/ORM/Mapping/OneToManyAssociationMapping.php +++ b/lib/Doctrine/ORM/Mapping/OneToManyAssociationMapping.php @@ -4,7 +4,7 @@ namespace Doctrine\ORM\Mapping; -final class OneToManyAssociationMapping extends ToManyAssociationMapping +final class OneToManyAssociationMapping extends ToManyInverseSideMapping { /** * @param mixed[] $mappingArray diff --git a/lib/Doctrine/ORM/Mapping/OneToOneAssociationMapping.php b/lib/Doctrine/ORM/Mapping/OneToOneAssociationMapping.php index 6faafc3ee51..89c6483e504 100644 --- a/lib/Doctrine/ORM/Mapping/OneToOneAssociationMapping.php +++ b/lib/Doctrine/ORM/Mapping/OneToOneAssociationMapping.php @@ -4,6 +4,6 @@ namespace Doctrine\ORM\Mapping; -class OneToOneAssociationMapping extends ToOneAssociationMapping +interface OneToOneAssociationMapping extends ToOneAssociationMapping { } diff --git a/lib/Doctrine/ORM/Mapping/OneToOneInverseSideMapping.php b/lib/Doctrine/ORM/Mapping/OneToOneInverseSideMapping.php new file mode 100644 index 00000000000..85e0f30dca0 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/OneToOneInverseSideMapping.php @@ -0,0 +1,9 @@ + */ - public array $sourceToTargetKeyColumns = []; - - /** @var array */ - public array $targetToSourceKeyColumns = []; - - /** @var list */ - public array $joinColumns = []; - - /** @return list */ - public function __sleep(): array - { - $serialized = parent::__sleep(); - - $serialized[] = 'joinColumns'; - $serialized[] = 'sourceToTargetKeyColumns'; - $serialized[] = 'targetToSourceKeyColumns'; - - return $serialized; - } } diff --git a/lib/Doctrine/ORM/Mapping/OwningSideMapping.php b/lib/Doctrine/ORM/Mapping/OwningSideMapping.php new file mode 100644 index 00000000000..ab8b7b2d7f1 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/OwningSideMapping.php @@ -0,0 +1,28 @@ + */ + public function __sleep(): array + { + $serialized = parent::__sleep(); + + if ($this->inversedBy !== null) { + $serialized[] = 'inversedBy'; + } + + return $serialized; + } +} diff --git a/lib/Doctrine/ORM/Mapping/ToManyAssociationMapping.php b/lib/Doctrine/ORM/Mapping/ToManyAssociationMapping.php index 1db301d0430..0f360149cfc 100644 --- a/lib/Doctrine/ORM/Mapping/ToManyAssociationMapping.php +++ b/lib/Doctrine/ORM/Mapping/ToManyAssociationMapping.php @@ -4,34 +4,6 @@ namespace Doctrine\ORM\Mapping; -abstract class ToManyAssociationMapping extends AssociationMapping +interface ToManyAssociationMapping { - /** - * Specification of a field on target-entity that is used to index the - * collection by. This field HAS to be either the primary key or a unique - * column. Otherwise the collection does not contain all the entities that - * are actually related. - */ - public string|null $indexBy = null; - - /** - * A map of field names (of the target entity) to sorting directions - * - * @var array|null - */ - public array|null $orderBy = null; - - /** @return list */ - public function __sleep(): array - { - $serialized = parent::__sleep(); - - foreach (['indexBy', 'orderBy'] as $stringOrArrayKey) { - if ($this->$stringOrArrayKey !== null) { - $serialized[] = $stringOrArrayKey; - } - } - - return $serialized; - } } diff --git a/lib/Doctrine/ORM/Mapping/ToManyAssociationMappingImplementation.php b/lib/Doctrine/ORM/Mapping/ToManyAssociationMappingImplementation.php new file mode 100644 index 00000000000..c7169894520 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/ToManyAssociationMappingImplementation.php @@ -0,0 +1,38 @@ +|null + */ + public array|null $orderBy = null; + + /** @return list */ + public function __sleep(): array + { + $serialized = parent::__sleep(); + + foreach (['indexBy', 'orderBy'] as $stringOrArrayKey) { + if ($this->$stringOrArrayKey !== null) { + $serialized[] = $stringOrArrayKey; + } + } + + return $serialized; + } +} diff --git a/lib/Doctrine/ORM/Mapping/ToManyInverseSideMapping.php b/lib/Doctrine/ORM/Mapping/ToManyInverseSideMapping.php new file mode 100644 index 00000000000..a092ebe31aa --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/ToManyInverseSideMapping.php @@ -0,0 +1,10 @@ + $mappingArray - * @psalm-param array{ - * fieldName: string, - * sourceEntity: class-string, - * targetEntity: class-string, - * joinColumns?: mixed[]|null, - * isOwningSide: bool, ...} $mappingArray - */ - public static function fromMappingArray(array $mappingArray): static - { - $joinColumns = $mappingArray['joinColumns'] ?? []; - - if (isset($mappingArray['joinColumns'])) { - unset($mappingArray['joinColumns']); - } - - $instance = parent::fromMappingArray($mappingArray); - - foreach ($joinColumns as $column) { - assert($instance->isToOneOwningSide()); - $instance->joinColumns[] = JoinColumnMapping::fromMappingArray($column); - } - - if ($instance->orphanRemoval) { - if (! $instance->isCascadeRemove()) { - $instance->cascade[] = 'remove'; - } - - $instance->unique = null; - } - - return $instance; - } - - /** - * @param mixed[] $mappingArray - * @param class-string $name - * @psalm-param array{ - * fieldName: string, - * sourceEntity: class-string, - * targetEntity: class-string, - * joinColumns?: mixed[]|null, - * isOwningSide: bool, ...} $mappingArray - */ - public static function fromMappingArrayAndName( - array $mappingArray, - NamingStrategy $namingStrategy, - string $name, - array|null $table, - bool $isInheritanceTypeSingleTable, - ): OneToOneAssociationMapping|ManyToOneAssociationMapping { - $mapping = static::fromMappingArray($mappingArray); - - if ($mapping->isOwningSide()) { - assert($mapping instanceof OneToOneOwningSideMapping || $mapping instanceof ManyToOneAssociationMapping); - if (empty($mapping->joinColumns)) { - // Apply default join column - $mapping->joinColumns = [ - JoinColumnMapping::fromMappingArray([ - 'name' => $namingStrategy->joinColumnName($mapping['fieldName'], $name), - 'referencedColumnName' => $namingStrategy->referenceColumnName(), - ]), - ]; - } - - $uniqueConstraintColumns = []; - - foreach ($mapping->joinColumns as $joinColumn) { - if ($mapping->isOneToOne() && ! $isInheritanceTypeSingleTable) { - if (count($mapping->joinColumns) === 1) { - if (empty($mapping->id)) { - $joinColumn->unique = true; - } - } else { - $uniqueConstraintColumns[] = $joinColumn->name; - } - } - - if (empty($joinColumn->name)) { - $joinColumn->name = $namingStrategy->joinColumnName($mapping->fieldName, $name); - } - - if (empty($joinColumn->referencedColumnName)) { - $joinColumn->referencedColumnName = $namingStrategy->referenceColumnName(); - } - - if ($joinColumn->name[0] === '`') { - $joinColumn->name = trim($joinColumn->name, '`'); - $joinColumn->quoted = true; - } - - if ($joinColumn->referencedColumnName[0] === '`') { - $joinColumn->referencedColumnName = trim($joinColumn->referencedColumnName, '`'); - $joinColumn->quoted = true; - } - - $mapping->sourceToTargetKeyColumns[$joinColumn->name] = $joinColumn->referencedColumnName; - $mapping->joinColumnFieldNames[$joinColumn->name] = $joinColumn->fieldName ?? $joinColumn->name; - } - - if ($uniqueConstraintColumns) { - if (! $table) { - throw new RuntimeException('ClassMetadata::setTable() has to be called before defining a one to one relationship.'); - } - - $table['uniqueConstraints'][$mapping->fieldName . '_uniq'] = ['columns' => $uniqueConstraintColumns]; - } - - $mapping->targetToSourceKeyColumns = array_flip($mapping->sourceToTargetKeyColumns); - } - - if (isset($mapping->id) && $mapping->id === true && ! $mapping->isOwningSide()) { - throw MappingException::illegalInverseIdentifierAssociation($name, $mapping->fieldName); - } - - return $mapping; - } - - public function offsetSet(mixed $offset, mixed $value): void - { - if ($offset === 'joinColumns') { - assert($this->isToOneOwningSide()); - $joinColumns = []; - foreach ($value as $column) { - $joinColumns[] = JoinColumnMapping::fromMappingArray($column); - } - - $this->joinColumns = $joinColumns; - - return; - } - - parent::offsetSet($offset, $value); - } - - /** @return array */ - public function toArray(): array - { - $array = parent::toArray(); - - if ($array['joinColumns'] !== []) { - $joinColumns = []; - foreach ($array['joinColumns'] as $column) { - $joinColumns[] = (array) $column; - } - - $array['joinColumns'] = $joinColumns; - } - - return $array; - } } diff --git a/lib/Doctrine/ORM/Mapping/ToOneInverseSideMapping.php b/lib/Doctrine/ORM/Mapping/ToOneInverseSideMapping.php new file mode 100644 index 00000000000..232952be438 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/ToOneInverseSideMapping.php @@ -0,0 +1,39 @@ +id) && $mapping->id === true) { + throw MappingException::illegalInverseIdentifierAssociation($name, $mapping->fieldName); + } + + if ($mapping->orphanRemoval) { + if (! $mapping->isCascadeRemove()) { + $mapping->cascade[] = 'remove'; + } + + $mapping->unique = null; + } + + return $mapping; + } +} diff --git a/lib/Doctrine/ORM/Mapping/ToOneOwningSideMapping.php b/lib/Doctrine/ORM/Mapping/ToOneOwningSideMapping.php new file mode 100644 index 00000000000..f356860f833 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/ToOneOwningSideMapping.php @@ -0,0 +1,176 @@ + */ + public array $sourceToTargetKeyColumns = []; + + /** @var array */ + public array $targetToSourceKeyColumns = []; + + /** @var list */ + public array $joinColumns = []; + + /** + * @param array $mappingArray + * @psalm-param array{ + * fieldName: string, + * sourceEntity: class-string, + * targetEntity: class-string, + * joinColumns?: mixed[]|null, + * isOwningSide: bool, ...} $mappingArray + */ + public static function fromMappingArray(array $mappingArray): static + { + $joinColumns = $mappingArray['joinColumns'] ?? []; + unset($mappingArray['joinColumns']); + + $instance = parent::fromMappingArray($mappingArray); + assert($instance->isToOneOwningSide()); + + foreach ($joinColumns as $column) { + $instance->joinColumns[] = JoinColumnMapping::fromMappingArray($column); + } + + if ($instance->orphanRemoval) { + if (! $instance->isCascadeRemove()) { + $instance->cascade[] = 'remove'; + } + + $instance->unique = null; + } + + return $instance; + } + + /** + * @param mixed[] $mappingArray + * @param class-string $name + * @psalm-param array{ + * fieldName: string, + * sourceEntity: class-string, + * targetEntity: class-string, + * joinColumns?: mixed[]|null, + * isOwningSide: bool, ...} $mappingArray + */ + public static function fromMappingArrayAndName( + array $mappingArray, + NamingStrategy $namingStrategy, + string $name, + array|null $table, + bool $isInheritanceTypeSingleTable, + ): static { + $mapping = static::fromMappingArray($mappingArray); + + assert($mapping->isToOneOwningSide()); + if (empty($mapping->joinColumns)) { + // Apply default join column + $mapping->joinColumns = [ + JoinColumnMapping::fromMappingArray([ + 'name' => $namingStrategy->joinColumnName($mapping['fieldName'], $name), + 'referencedColumnName' => $namingStrategy->referenceColumnName(), + ]), + ]; + } + + $uniqueConstraintColumns = []; + + foreach ($mapping->joinColumns as $joinColumn) { + if ($mapping->isOneToOne() && ! $isInheritanceTypeSingleTable) { + if (count($mapping->joinColumns) === 1) { + if (empty($mapping->id)) { + $joinColumn->unique = true; + } + } else { + $uniqueConstraintColumns[] = $joinColumn->name; + } + } + + if (empty($joinColumn->name)) { + $joinColumn->name = $namingStrategy->joinColumnName($mapping->fieldName, $name); + } + + if (empty($joinColumn->referencedColumnName)) { + $joinColumn->referencedColumnName = $namingStrategy->referenceColumnName(); + } + + if ($joinColumn->name[0] === '`') { + $joinColumn->name = trim($joinColumn->name, '`'); + $joinColumn->quoted = true; + } + + if ($joinColumn->referencedColumnName[0] === '`') { + $joinColumn->referencedColumnName = trim($joinColumn->referencedColumnName, '`'); + $joinColumn->quoted = true; + } + + $mapping->sourceToTargetKeyColumns[$joinColumn->name] = $joinColumn->referencedColumnName; + $mapping->joinColumnFieldNames[$joinColumn->name] = $joinColumn->fieldName ?? $joinColumn->name; + } + + if ($uniqueConstraintColumns) { + if (! $table) { + throw new RuntimeException('ClassMetadata::setTable() has to be called before defining a one to one relationship.'); + } + + $table['uniqueConstraints'][$mapping->fieldName . '_uniq'] = ['columns' => $uniqueConstraintColumns]; + } + + $mapping->targetToSourceKeyColumns = array_flip($mapping->sourceToTargetKeyColumns); + + return $mapping; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + if ($offset === 'joinColumns') { + $joinColumns = []; + foreach ($value as $column) { + $joinColumns[] = JoinColumnMapping::fromMappingArray($column); + } + + $this->joinColumns = $joinColumns; + + return; + } + + parent::offsetSet($offset, $value); + } + + /** @return array */ + public function toArray(): array + { + $array = parent::toArray(); + + $joinColumns = []; + foreach ($array['joinColumns'] as $column) { + $joinColumns[] = (array) $column; + } + + $array['joinColumns'] = $joinColumns; + + return $array; + } + + /** @return list */ + public function __sleep(): array + { + return [ + ...parent::__sleep(), + 'joinColumns', + 'sourceToTargetKeyColumns', + 'targetToSourceKeyColumns', + ]; + } +} diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 57a17079082..8634e54c809 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -102,7 +102,7 @@ public function setOwner(object $entity, AssociationMapping $assoc): void { $this->owner = $entity; $this->association = $assoc; - $this->backRefFieldName = $assoc['inversedBy'] ?: $assoc['mappedBy']; + $this->backRefFieldName = $assoc->isOwningSide() ? $assoc->inversedBy : $assoc->mappedBy; } /** diff --git a/lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php index d70900662c5..c6e2fe3253d 100644 --- a/lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php @@ -11,6 +11,7 @@ use Doctrine\DBAL\LockMode; use Doctrine\ORM\Mapping\AssociationMapping; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\InverseSideMapping; use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Persisters\SqlValueVisitor; use Doctrine\ORM\Query; @@ -223,6 +224,7 @@ public function loadCriteria(PersistentCollection $collection, Criteria $criteri $paramTypes = []; if (! $mapping->isOwningSide()) { + assert($mapping instanceof InverseSideMapping); $associationSourceClass = $targetClass; $mapping = $targetClass->associationMappings[$mapping->mappedBy]; $sourceRelationMode = 'relationToTargetKeyColumns'; @@ -572,14 +574,14 @@ private function getJoinTableRestrictionsWithKey( ): array { $filterMapping = $collection->getMapping(); $mapping = $filterMapping; - assert($mapping->isManyToMany()); - $indexBy = $mapping->indexBy; - assert($indexBy !== null); + assert(isset($mapping->indexBy)); + $indexBy = $mapping->indexBy; $id = $this->uow->getEntityIdentifier($collection->getOwner()); $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); $targetClass = $this->em->getClassMetadata($mapping->targetEntity); if (! $mapping->isOwningSide()) { + assert($mapping instanceof InverseSideMapping); $associationSourceClass = $this->em->getClassMetadata($mapping->targetEntity); $mapping = $associationSourceClass->associationMappings[$mapping->mappedBy]; assert($mapping->isManyToManyOwningSide()); diff --git a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php index 35e8429fb4a..46875005477 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php @@ -19,6 +19,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\AssociationMapping; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\InverseSideMapping; use Doctrine\ORM\Mapping\JoinColumnMapping; use Doctrine\ORM\Mapping\MappingException; use Doctrine\ORM\Mapping\OneToManyAssociationMapping; @@ -512,7 +513,7 @@ protected function deleteJoinTableRecords(array $identifier, array $types): void $keys = []; if (! $mapping->isOwningSide()) { - assert(isset($mapping->mappedBy)); + assert($mapping instanceof InverseSideMapping); $class = $this->em->getClassMetadata($mapping->targetEntity); $association = $class->associationMappings[$mapping->mappedBy]; } @@ -1746,9 +1747,8 @@ private function getOneToManyStatement( ): Result { $this->switchPersisterContext($offset, $limit); - $criteria = []; - $parameters = []; - assert(isset($assoc->mappedBy)); + $criteria = []; + $parameters = []; $owningAssoc = $this->class->associationMappings[$assoc->mappedBy]; $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity); $tableAlias = $this->getSQLTableAlias($owningAssoc->inherited ?? $this->class->name); diff --git a/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php index 436967fc0f6..272b90f6144 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php @@ -61,7 +61,6 @@ public function getSql(SqlWalker $sqlWalker): string . $sourceTableAlias . '.' . $quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $platform); } } else { // many-to-many - assert($assoc->isManyToMany()); $targetClass = $entityManager->getClassMetadata($assoc->targetEntity); $owningAssoc = $assoc->isOwningSide() ? $assoc : $targetClass->associationMappings[$assoc['mappedBy']]; diff --git a/lib/Doctrine/ORM/Tools/SchemaValidator.php b/lib/Doctrine/ORM/Tools/SchemaValidator.php index 9222f9ba8a4..6c9baf9a9a4 100644 --- a/lib/Doctrine/ORM/Tools/SchemaValidator.php +++ b/lib/Doctrine/ORM/Tools/SchemaValidator.php @@ -95,16 +95,12 @@ public function validateClass(ClassMetadata $class): array return $ce; } - if ($assoc['mappedBy'] && $assoc['inversedBy']) { - $ce[] = 'The association ' . $class . '#' . $fieldName . ' cannot be defined as both inverse and owning.'; - } - if (isset($assoc['id']) && $targetMetadata->containsForeignIdentifier) { $ce[] = "Cannot map association '" . $class->name . '#' . $fieldName . ' as identifier, because ' . "the target entity '" . $targetMetadata->name . "' also maps an association as identifier."; } - if ($assoc['mappedBy']) { + if (! $assoc->isOwningSide()) { if ($targetMetadata->hasField($assoc['mappedBy'])) { $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' . 'field ' . $assoc['targetEntity'] . '#' . $assoc['mappedBy'] . ' which is not defined as association, but as field.'; @@ -125,7 +121,7 @@ public function validateClass(ClassMetadata $class): array } } - if ($assoc->inversedBy) { + if ($assoc->isOwningSide() && $assoc->inversedBy) { if ($targetMetadata->hasField($assoc['inversedBy'])) { $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' . 'field ' . $assoc['targetEntity'] . '#' . $assoc['inversedBy'] . ' which is not defined as association.'; @@ -134,7 +130,7 @@ public function validateClass(ClassMetadata $class): array if (! $targetMetadata->hasAssociation($assoc['inversedBy'])) { $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' . 'field ' . $assoc['targetEntity'] . '#' . $assoc['inversedBy'] . ' which does not exist.'; - } elseif ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] === null) { + } elseif ($targetMetadata->associationMappings[$assoc['inversedBy']]->isOwningSide()) { $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the owning side of a ' . 'bi-directional relationship, but the specified inversedBy association on the target-entity ' . $assoc['targetEntity'] . '#' . $assoc['inversedBy'] . ' does not contain the required ' . diff --git a/phpstan.neon b/phpstan.neon index d3bd7dee0cf..cb11aefbd29 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -26,19 +26,13 @@ parameters: # https://github.com/phpstan/phpstan/issues/8904 - - message: "#^Access to an undefined property Doctrine\\\\ORM\\\\Mapping\\\\ToOneAssociationMapping\\:\\:\\$joinColumns\\.$#" - path: lib/Doctrine/ORM/Mapping/ToOneAssociationMapping.php - - - - message: "#^Access to an undefined property .*Mapping\\:\\:\\$(joinColumns|joinTableColumns|(relation|source|target)To(Target|Source)KeyColumns|joinTable|indexBy)\\.$#" + message: "#^Access to an undefined property .*Mapping\\:\\:\\$(inversedBy|mappedBy|joinColumns|joinTableColumns|(relation|source|target)To(Target|Source)KeyColumns|joinTable|indexBy)\\.$#" paths: - lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php - lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php - lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php + - lib/Doctrine/ORM/PersistentCollection.php - lib/Doctrine/ORM/Query/AST/Functions/IdentityFunction.php - lib/Doctrine/ORM/Query/SqlWalker.php + - lib/Doctrine/ORM/Tools/SchemaValidator.php - lib/Doctrine/ORM/UnitOfWork.php - - - - message: "#^Method Doctrine\\\\ORM\\\\Mapping\\\\ToOneAssociationMapping\\:\\:fromMappingArrayAndName\\(\\) should return Doctrine\\\\ORM\\\\Mapping\\\\ManyToOneAssociationMapping\\|Doctrine\\\\ORM\\\\Mapping\\\\OneToOneAssociationMapping but returns static\\(Doctrine\\\\ORM\\\\Mapping\\\\ToOneAssociationMapping\\)\\.$#" - path: lib/Doctrine/ORM/Mapping/ToOneAssociationMapping.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 5a99e7563fe..a809e4fd06f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -283,6 +283,14 @@ + + + $mapping + + + static + + $repositoryClassName @@ -304,6 +312,7 @@ $mapping $mapping $mapping + $mapping $overrideMapping @@ -570,6 +579,11 @@ getName() === 'mapped-superclass']]> + + + ManyToManyInverseSideMapping + + $joinTable @@ -581,6 +595,19 @@ new JoinTableMapping() + + + OneToManyAssociationMapping + + + mappedBy)]]> + + + + + OneToOneInverseSideMapping + + $object @@ -605,13 +632,21 @@ ReflectionReadonlyProperty - + + + $mapping + + + static + + + $instance $mapping - OneToOneAssociationMapping|ManyToOneAssociationMapping + static static diff --git a/psalm.xml b/psalm.xml index 66ec1f169e5..fc0232ccdb0 100644 --- a/psalm.xml +++ b/psalm.xml @@ -202,6 +202,13 @@ + + + + + + + diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/Entity/EntityPersisterTestCase.php b/tests/Doctrine/Tests/ORM/Cache/Persister/Entity/EntityPersisterTestCase.php index ca67a5a8b61..ab122fee553 100644 --- a/tests/Doctrine/Tests/ORM/Cache/Persister/Entity/EntityPersisterTestCase.php +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/Entity/EntityPersisterTestCase.php @@ -12,7 +12,7 @@ use Doctrine\ORM\Cache\Region; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Mapping\OneToOneAssociationMapping; +use Doctrine\ORM\Mapping\OneToOneInverseSideMapping; use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Persisters\Entity\EntityPersister; use Doctrine\ORM\Query\ResultSetMappingBuilder; @@ -90,7 +90,7 @@ public function testInvokeGetSelectSQL(): void { $persister = $this->createPersisterDefault(); - $associationMapping = new OneToOneAssociationMapping('foo', 'bar', 'baz'); + $associationMapping = new OneToOneInverseSideMapping('foo', 'bar', 'baz'); $this->entityPersister->expects(self::once()) ->method('getSelectSQL') @@ -154,7 +154,7 @@ public function testInvokeSelectConditionStatementSQL(): void { $persister = $this->createPersisterDefault(); - $associationMapping = new OneToOneAssociationMapping('foo', 'bar', 'baz'); + $associationMapping = new OneToOneInverseSideMapping('foo', 'bar', 'baz'); $this->entityPersister->expects(self::once()) ->method('getSelectConditionStatementSQL') @@ -225,7 +225,7 @@ public function testInvokeLoad(): void $persister = $this->createPersisterDefault(); $entity = new Country('Foo'); - $associationMapping = new OneToOneAssociationMapping('foo', 'bar', 'baz'); + $associationMapping = new OneToOneInverseSideMapping('foo', 'bar', 'baz'); $this->entityPersister->expects(self::once()) ->method('load') @@ -285,7 +285,7 @@ public function testInvokeLoadOneToOneEntity(): void $entity = new Country('Foo'); $owner = (object) []; - $associationMapping = new OneToOneAssociationMapping('foo', 'bar', 'baz'); + $associationMapping = new OneToOneInverseSideMapping('foo', 'bar', 'baz'); $this->entityPersister->expects(self::once()) ->method('loadOneToOneEntity') @@ -336,7 +336,7 @@ public function testInvokeGetManyToManyCollection(): void $entity = new Country('Foo'); $owner = (object) []; - $associationMapping = new OneToOneAssociationMapping('foo', 'bar', 'baz'); + $associationMapping = new OneToOneInverseSideMapping('foo', 'bar', 'baz'); $this->entityPersister->expects(self::once()) ->method('getManyToManyCollection') @@ -352,7 +352,7 @@ public function testInvokeGetOneToManyCollection(): void $entity = new Country('Foo'); $owner = (object) []; - $associationMapping = new OneToOneAssociationMapping('foo', 'bar', 'baz'); + $associationMapping = new OneToOneInverseSideMapping('foo', 'bar', 'baz'); $this->entityPersister->expects(self::once()) ->method('getOneToManyCollection') @@ -365,7 +365,7 @@ public function testInvokeGetOneToManyCollection(): void public function testInvokeLoadManyToManyCollection(): void { $mapping = $this->em->getClassMetadata(Country::class); - $assoc = new OneToOneAssociationMapping('foo', 'bar', 'baz'); + $assoc = new OneToOneInverseSideMapping('foo', 'bar', 'baz'); $coll = new PersistentCollection($this->em, $mapping, new ArrayCollection()); $persister = $this->createPersisterDefault(); $entity = new Country('Foo'); @@ -382,7 +382,7 @@ public function testInvokeLoadManyToManyCollection(): void public function testInvokeLoadOneToManyCollection(): void { $mapping = $this->em->getClassMetadata(Country::class); - $assoc = new OneToOneAssociationMapping('foo', 'bar', 'baz'); + $assoc = new OneToOneInverseSideMapping('foo', 'bar', 'baz'); $coll = new PersistentCollection($this->em, $mapping, new ArrayCollection()); $persister = $this->createPersisterDefault(); $entity = new Country('Foo'); diff --git a/tests/Doctrine/Tests/ORM/Mapping/AssociationMappingTest.php b/tests/Doctrine/Tests/ORM/Mapping/AssociationMappingTest.php index b53e23e20a0..facc680be32 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AssociationMappingTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AssociationMappingTest.php @@ -23,8 +23,6 @@ public function testItSurvivesSerialization(): void targetEntity: self::class, ); - $mapping->mappedBy = 'foo'; - $mapping->inversedBy = 'bar'; $mapping->cascade = ['persist']; $mapping->fetch = ClassMetadata::FETCH_EAGER; $mapping->inherited = self::class; @@ -41,8 +39,6 @@ public function testItSurvivesSerialization(): void $resurrectedMapping = unserialize(serialize($mapping)); assert($resurrectedMapping instanceof AssociationMapping); - self::assertSame('foo', $resurrectedMapping->mappedBy); - self::assertSame('bar', $resurrectedMapping->inversedBy); self::assertSame(['persist'], $resurrectedMapping->cascade); self::assertSame(ClassMetadata::FETCH_EAGER, $resurrectedMapping->fetch); self::assertSame(self::class, $resurrectedMapping->inherited); diff --git a/tests/Doctrine/Tests/ORM/Mapping/InverseSideMappingTest.php b/tests/Doctrine/Tests/ORM/Mapping/InverseSideMappingTest.php new file mode 100644 index 00000000000..b5a7cd74127 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/InverseSideMappingTest.php @@ -0,0 +1,35 @@ +mappedBy = 'bar'; + + $resurrectedMapping = unserialize(serialize($mapping)); + assert($resurrectedMapping instanceof InverseSideMapping); + + self::assertSame('bar', $resurrectedMapping->mappedBy); + } +} + +class MyInverseAssociationMapping extends InverseSideMapping +{ +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/MappingDriverTestCase.php b/tests/Doctrine/Tests/ORM/Mapping/MappingDriverTestCase.php index 0f2a9e3934b..e3abecf83e3 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/MappingDriverTestCase.php +++ b/tests/Doctrine/Tests/ORM/Mapping/MappingDriverTestCase.php @@ -624,7 +624,6 @@ public function testAssociationOverridesMapping(): void // assert not override attributes self::assertEquals($guestGroups['fieldName'], $adminGroups['fieldName']); self::assertEquals($guestGroups['type'], $adminGroups['type']); - self::assertEquals($guestGroups['mappedBy'], $adminGroups['mappedBy']); self::assertEquals($guestGroups['inversedBy'], $adminGroups['inversedBy']); self::assertEquals($guestGroups['isOwningSide'], $adminGroups['isOwningSide']); self::assertEquals($guestGroups['fetch'], $adminGroups['fetch']); @@ -661,7 +660,6 @@ public function testAssociationOverridesMapping(): void // assert not override attributes self::assertEquals($guestAddress['fieldName'], $adminAddress['fieldName']); self::assertEquals($guestAddress['type'], $adminAddress['type']); - self::assertEquals($guestAddress['mappedBy'], $adminAddress['mappedBy']); self::assertEquals($guestAddress['inversedBy'], $adminAddress['inversedBy']); self::assertEquals($guestAddress['isOwningSide'], $adminAddress['isOwningSide']); self::assertEquals($guestAddress['fetch'], $adminAddress['fetch']); diff --git a/tests/Doctrine/Tests/ORM/Mapping/OwningSideMappingTest.php b/tests/Doctrine/Tests/ORM/Mapping/OwningSideMappingTest.php new file mode 100644 index 00000000000..21289db949a --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/OwningSideMappingTest.php @@ -0,0 +1,35 @@ +inversedBy = 'bar'; + + $resurrectedMapping = unserialize(serialize($mapping)); + assert($resurrectedMapping instanceof OwningSideMapping); + + self::assertSame('bar', $resurrectedMapping->inversedBy); + } +} + +class MyOwningAssociationMapping extends OwningSideMapping +{ +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/ToManyAssociationMappingTest.php b/tests/Doctrine/Tests/ORM/Mapping/ToManyAssociationMappingTest.php index 8befdde20f0..e435a944b7f 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ToManyAssociationMappingTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ToManyAssociationMappingTest.php @@ -4,7 +4,9 @@ namespace Doctrine\Tests\ORM\Mapping; +use Doctrine\ORM\Mapping\AssociationMapping; use Doctrine\ORM\Mapping\ToManyAssociationMapping; +use Doctrine\ORM\Mapping\ToManyAssociationMappingImplementation; use PHPUnit\Framework\TestCase; use function assert; @@ -32,6 +34,7 @@ public function testItSurvivesSerialization(): void } } -class MyToManyAssociationMapping extends ToManyAssociationMapping +class MyToManyAssociationMapping extends AssociationMapping implements ToManyAssociationMapping { + use ToManyAssociationMappingImplementation; }