From a9b6b720175fae69e90247e6b7a0e34d7da2f1cc Mon Sep 17 00:00:00 2001 From: Maciej Malarz Date: Sun, 15 Mar 2020 01:00:58 +0100 Subject: [PATCH] Fix inherited embeddables and nesting after AnnotationDriver change #8006 (#8036) * Add test case * Treat parent embeddables as mapped superclasses * [GH-8031] Bugfix: Get working again on nested embeddables in inherited embeddables. * Housekeeping: CS * Update note on limitations * [GH-8031] Verify assocations still do not work with Embeddables. * Housekeeping: CS Co-authored-by: Benjamin Eberlei --- docs/en/tutorials/embeddables.rst | 4 +- .../ORM/Mapping/ClassMetadataFactory.php | 13 +- .../ORM/Mapping/Driver/AnnotationDriver.php | 2 + .../ORM/Functional/Ticket/GH8031Test.php | 163 ++++++++++++++++++ 4 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/GH8031Test.php diff --git a/docs/en/tutorials/embeddables.rst b/docs/en/tutorials/embeddables.rst index 483e58d9da6..c49bdfc65af 100644 --- a/docs/en/tutorials/embeddables.rst +++ b/docs/en/tutorials/embeddables.rst @@ -8,7 +8,9 @@ or address are the primary use case for this feature. .. note:: - Embeddables can only contain properties with basic ``@Column`` mapping. + Embeddables can not contain references to entities. They can however compose + other embeddables in addition to holding properties with basic ``@Column`` + mapping. For the purposes of this tutorial, we will assume that you have a ``User`` class in your application and you would like to store an address in diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 8410ce5b0de..e1142e41bf1 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -31,6 +31,7 @@ use Doctrine\ORM\Id\IdentityGenerator; use Doctrine\ORM\ORMException; use ReflectionException; +use function assert; /** * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the @@ -401,10 +402,10 @@ private function getShortName($className) private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass) { foreach ($parentClass->fieldMappings as $mapping) { - if ( ! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) { + if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass && ! $parentClass->isEmbeddedClass) { $mapping['inherited'] = $parentClass->name; } - if ( ! isset($mapping['declared'])) { + if (! isset($mapping['declared'])) { $mapping['declared'] = $parentClass->name; } $subClass->addInheritedFieldMapping($mapping); @@ -469,10 +470,6 @@ private function addInheritedEmbeddedClasses(ClassMetadata $subClass, ClassMetad private function addNestedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass, $prefix) { foreach ($subClass->embeddedClasses as $property => $embeddableClass) { - if (isset($embeddableClass['inherited'])) { - continue; - } - $embeddableMetadata = $this->getMetadataFor($embeddableClass['class']); $parentClass->mapEmbedded( @@ -780,7 +777,9 @@ protected function getDriver() */ protected function isEntity(ClassMetadataInterface $class) { - return isset($class->isMappedSuperclass) && $class->isMappedSuperclass === false; + assert($class instanceof ClassMetadata); + + return $class->isMappedSuperclass === false && $class->isEmbeddedClass === false; } /** diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index bafd5d27499..28cf958a315 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -277,6 +277,8 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) /* @var $property \ReflectionProperty */ foreach ($class->getProperties() as $property) { if ($metadata->isMappedSuperclass && ! $property->isPrivate() + || + $metadata->isEmbeddedClass && $property->getDeclaringClass()->getName() !== $class->getName() || $metadata->isInheritedField($property->name) || diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH8031Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH8031Test.php new file mode 100644 index 00000000000..7ae17d3f2f8 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH8031Test.php @@ -0,0 +1,163 @@ +setUpEntitySchema([ + GH8031Invoice::class, + ]); + } + + public function testEntityIsFetched() + { + $entity = new GH8031Invoice(new GH8031InvoiceCode(1, 2020, new GH8031Nested(10))); + $this->_em->persist($entity); + $this->_em->flush(); + $this->_em->clear(); + + /** @var GH8031Invoice $fetched */ + $fetched = $this->_em->find(GH8031Invoice::class, $entity->getId()); + $this->assertInstanceOf(GH8031Invoice::class, $fetched); + $this->assertSame(1, $fetched->getCode()->getNumber()); + $this->assertSame(2020, $fetched->getCode()->getYear()); + + $this->_em->clear(); + $this->assertCount( + 1, + $this->_em->getRepository(GH8031Invoice::class)->findBy([], ['code.number' => 'ASC']) + ); + } + + public function testEmbeddableWithAssociationNotAllowed() + { + $cm = $this->_em->getClassMetadata(GH8031EmbeddableWithAssociation::class); + + $this->assertArrayHasKey('invoice', $cm->associationMappings); + + $cm = $this->_em->getClassMetadata(GH8031Invoice::class); + + $this->assertCount(0, $cm->associationMappings); + } +} + +/** + * @Embeddable + */ +class GH8031EmbeddableWithAssociation +{ + /** @ManyToOne(targetEntity=GH8031Invoice::class) */ + public $invoice; +} + +/** + * @Embeddable + */ +class GH8031Nested +{ + /** + * @Column(type="integer", name="number", length=6) + * @var int + */ + protected $number; + + public function __construct(int $number) + { + $this->number = $number; + } + + public function getNumber() : int + { + return $this->number; + } +} + +/** + * @Embeddable + */ +class GH8031InvoiceCode extends GH8031AbstractYearSequenceValue +{ +} + +/** + * @Embeddable + */ +abstract class GH8031AbstractYearSequenceValue +{ + /** + * @Column(type="integer", name="number", length=6) + * @var int + */ + protected $number; + + /** + * @Column(type="smallint", name="year", length=4) + * @var int + */ + protected $year; + + /** @Embedded(class=GH8031Nested::class) */ + protected $nested; + + public function __construct(int $number, int $year, GH8031Nested $nested) + { + $this->number = $number; + $this->year = $year; + $this->nested = $nested; + } + + public function getNumber() : int + { + return $this->number; + } + + public function getYear() : int + { + return $this->year; + } +} + +/** + * @Entity + */ +class GH8031Invoice +{ + /** + * @Id + * @GeneratedValue + * @Column(type="integer") + */ + private $id; + + /** + * @Embedded(class=GH8031InvoiceCode::class) + * @var GH8031InvoiceCode + */ + private $code; + + /** @Embedded(class=GH8031EmbeddableWithAssociation::class) */ + private $embeddedAssoc; + + public function __construct(GH8031InvoiceCode $code) + { + $this->code = $code; + } + + public function getId() + { + return $this->id; + } + + public function getCode() : GH8031InvoiceCode + { + return $this->code; + } +}