diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 495218e5550..29aca9be230 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -384,14 +384,6 @@ private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $pare private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $parentClass): void { foreach ($parentClass->associationMappings as $field => $mapping) { - if ($parentClass->isMappedSuperclass) { - if ($mapping['type'] & ClassMetadata::TO_MANY && ! $mapping['isOwningSide']) { - throw MappingException::illegalToManyAssociationOnMappedSuperclass($parentClass->name, $field); - } - - $mapping['sourceEntity'] = $subClass->name; - } - if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) { $mapping['inherited'] = $parentClass->name; } @@ -400,6 +392,20 @@ private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $p $mapping['declared'] = $parentClass->name; } + // When the class inheriting the relation ($subClass) is the first entity class since the + // relation has been defined in a mapped superclass (or in a chain + // of mapped superclasses) above, then declare this current entity class as the source of + // the relationship. + // According to the definitions given in https://github.com/doctrine/orm/pull/10396/, + // this is the case <=> ! isset($mapping['inherited']). + if (! isset($mapping['inherited'])) { + if ($mapping['type'] & ClassMetadata::TO_MANY && ! $mapping['isOwningSide']) { + throw MappingException::illegalToManyAssociationOnMappedSuperclass($parentClass->name, $field); + } + + $mapping['sourceEntity'] = $subClass->name; + } + $subClass->addInheritedAssociationMapping($mapping); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH5998Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH5998Test.php new file mode 100644 index 00000000000..028bced80ba --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH5998Test.php @@ -0,0 +1,255 @@ +_schemaTool->createSchema([ + $this->_em->getClassMetadata(GH5998JTI::class), + $this->_em->getClassMetadata(GH5998JTIChild::class), + $this->_em->getClassMetadata(GH5998STI::class), + $this->_em->getClassMetadata(GH5998Basic::class), + $this->_em->getClassMetadata(GH5998Related::class), + ]); + } + + /** + * Verifies that MappedSuperclasses work within an inheritance hierarchy. + */ + public function testIssue(): void + { + // Test JTI + $this->classTests(GH5998JTIChild::class); + // Test STI + $this->classTests(GH5998STIChild::class); + // Test Basic + $this->classTests(GH5998Basic::class); + } + + private function classTests($className): void + { + // Test insert + $child = new $className('Sam', 0, 1); + $child->rel = new GH5998Related(); + $this->_em->persist($child); + $this->_em->persist($child->rel); + $this->_em->flush(); + $this->_em->clear(); + + // Test find by rel + $child = $this->_em->getRepository($className)->findOneBy(['rel' => $child->rel]); + self::assertNotNull($child); + $this->_em->clear(); + + // Test query by id with fetch join + $child = $this->_em->createQuery('SELECT t, r FROM ' . $className . ' t JOIN t.rel r WHERE t.id = 1')->getOneOrNullResult(); + self::assertNotNull($child); + + // Test lock and update + $this->_em->transactional(static function ($em) use ($child): void { + $em->lock($child, LockMode::NONE); + $child->firstName = 'Bob'; + $child->status = 0; + }); + $this->_em->clear(); + $child = $this->_em->getRepository($className)->find(1); + self::assertEquals($child->firstName, 'Bob'); + self::assertEquals($child->status, 0); + + // Test delete + $this->_em->remove($child); + $this->_em->flush(); + $child = $this->_em->getRepository($className)->find(1); + self::assertNull($child); + } +} + +/** + * @ORM\MappedSuperclass + */ +class GH5998Common +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; + /** + * @ORM\ManyToOne(targetEntity=GH5998Related::class) + * @ORM\JoinColumn(name="related_id", referencedColumnName="id") + * + * @var GH5998Related + */ + public $rel; + /** + * @ORM\Version + * @ORM\Column(type="integer") + * + * @var int + */ + public $version; + + /** @var mixed */ + public $other; +} + +/** + * @ORM\Entity + * @ORM\InheritanceType("JOINED") + * @ORM\DiscriminatorMap({"child" = GH5998JTIChild::class}) + */ +abstract class GH5998JTI extends GH5998Common +{ + /** + * @ORM\Column(type="string", length=255) + * + * @var string + */ + public $firstName; +} + +/** + * @ORM\MappedSuperclass + */ +class GH5998JTICommon extends GH5998JTI +{ + /** + * @ORM\Column(type="integer") + * + * @var int + */ + public $status; +} + +/** + * @ORM\Entity + */ +class GH5998JTIChild extends GH5998JTICommon +{ + /** + * @ORM\Column(type="integer") + * + * @var int + */ + public $type; + + public function __construct(string $firstName, int $type, int $status) + { + $this->firstName = $firstName; + $this->type = $type; + $this->status = $status; + } +} + +/** + * @ORM\Entity + * @ORM\InheritanceType("SINGLE_TABLE") + * @ORM\DiscriminatorMap({"child" = GH5998STIChild::class}) + */ +abstract class GH5998STI extends GH5998Common +{ + /** + * @ORM\Column(type="string", length=255) + * + * @var string + */ + public $firstName; +} + +/** + * @ORM\MappedSuperclass + */ +class GH5998STICommon extends GH5998STI +{ + /** + * @ORM\Column(type="integer") + * + * @var int + */ + public $status; +} + +/** + * @ORM\Entity + */ +class GH5998STIChild extends GH5998STICommon +{ + /** + * @ORM\Column(type="integer") + * + * @var int + */ + public $type; + + public function __construct(string $firstName, int $type, int $status) + { + $this->firstName = $firstName; + $this->type = $type; + $this->status = $status; + } +} + +/** + * @ORM\Entity + */ +class GH5998Basic extends GH5998Common +{ + /** + * @ORM\Column(type="string", length=255) + * + * @var string + */ + public $firstName; + + /** + * @ORM\Column(type="integer") + * + * @var int + */ + public $status; + + /** + * @ORM\Column(type="integer") + * + * @var int + */ + public $type; + + public function __construct(string $firstName, int $type, int $status) + { + $this->firstName = $firstName; + $this->type = $type; + $this->status = $status; + } +} + +/** + * @ORM\Entity() + */ +class GH5998Related +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH8415Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH8415Test.php new file mode 100644 index 00000000000..9ce30802660 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH8415Test.php @@ -0,0 +1,121 @@ +setUpEntitySchema( + [ + GH8415BaseClass::class, + GH8415MiddleMappedSuperclass::class, + GH8415LeafClass::class, + GH8415AssociationTarget::class, + ] + ); + } + + public function testAssociationIsBasedOnBaseClass(): void + { + $target = new GH8415AssociationTarget(); + $leaf = new GH8415LeafClass(); + $leaf->baseField = 'base'; + $leaf->middleField = 'middle'; + $leaf->leafField = 'leaf'; + $leaf->target = $target; + + $this->_em->persist($target); + $this->_em->persist($leaf); + $this->_em->flush(); + $this->_em->clear(); + + $query = $this->_em->createQuery('SELECT leaf FROM Doctrine\Tests\ORM\Functional\Ticket\GH8415LeafClass leaf JOIN leaf.target t'); + $result = $query->getOneOrNullResult(); + + $this->assertInstanceOf(GH8415LeafClass::class, $result); + $this->assertSame('base', $result->baseField); + $this->assertSame('middle', $result->middleField); + $this->assertSame('leaf', $result->leafField); + } +} + +/** + * @ORM\Entity + */ +class GH8415AssociationTarget +{ + /** + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue + * + * @var int + */ + public $id; +} + +/** + * @ORM\Entity + * @ORM\InheritanceType("JOINED") + * @ORM\DiscriminatorColumn(name="discriminator", type="string") + * @ORM\DiscriminatorMap({"1" = "Doctrine\Tests\ORM\Functional\Ticket\GH8415BaseClass", "2" = "Doctrine\Tests\ORM\Functional\Ticket\GH8415LeafClass"}) + */ +class GH8415BaseClass +{ + /** + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue + * + * @var int + */ + public $id; + + /** + * @ORM\ManyToOne(targetEntity="GH8415AssociationTarget") + * + * @var GH8415AssociationTarget + */ + public $target; + + /** + * @ORM\Column(type="string") + * + * @var string + */ + public $baseField; +} + +/** + * @ORM\MappedSuperclass + */ +class GH8415MiddleMappedSuperclass extends GH8415BaseClass +{ + /** + * @ORM\Column(type="string") + * + * @var string + */ + public $middleField; +} + +/** + * @ORM\Entity + */ +class GH8415LeafClass extends GH8415MiddleMappedSuperclass +{ + /** + * @ORM\Column(type="string") + * + * @var string + */ + public $leafField; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH8415ToManyAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH8415ToManyAssociationTest.php new file mode 100644 index 00000000000..c5a7c35f3df --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH8415ToManyAssociationTest.php @@ -0,0 +1,87 @@ +expectNotToPerformAssertions(); + + $em = $this->getTestEntityManager(); + $em->getClassMetadata(GH8415ToManyLeafClass::class); + } +} + +/** + * @ORM\Entity + */ +class GH8415ToManyAssociationTarget +{ + /** + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue + * + * @var int + */ + public $id; + + /** + * @ORM\ManyToOne(targetEntity="GH8415ToManyBaseClass", inversedBy="targets") + * + * @var GH8415ToManyBaseClass + */ + public $base; +} + +/** + * @ORM\Entity + * @ORM\InheritanceType("SINGLE_TABLE") + * @ORM\DiscriminatorColumn(name="discriminator", type="string") + * @ORM\DiscriminatorMap({"1" = "Doctrine\Tests\ORM\Functional\Ticket\GH8415ToManyBaseClass", "2" = "Doctrine\Tests\ORM\Functional\Ticket\GH8415ToManyLeafClass"}) + */ +class GH8415ToManyBaseClass +{ + /** + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue + * + * @var int + */ + public $id; + + /** + * @ORM\OneToMany(targetEntity="GH8415ToManyAssociationTarget", mappedBy="base") + * + * @var Collection + */ + public $targets; +} + +/** + * @ORM\MappedSuperclass + */ +class GH8415ToManyMappedSuperclass extends GH8415ToManyBaseClass +{ +} + +/** + * @ORM\Entity + */ +class GH8415ToManyLeafClass extends GH8415ToManyMappedSuperclass +{ + /** + * @ORM\Column(type="string") + * + * @var string + */ + public $leafField; +}