From 23f4f0357530b46a76bae26a194bd50754cc5fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Va=C5=A1ek=20Henzl?= Date: Wed, 13 Sep 2017 16:15:31 +1200 Subject: [PATCH 1/2] Add failing tests for #6531 Tests are based on examples from "Composite and Foreign Keys as Primary Key" tutorial: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/composite-primary-keys.html --- .../ORM/Functional/Ticket/GH6531Test.php | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/GH6531Test.php diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6531Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6531Test.php new file mode 100644 index 000000000..1d819f250 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH6531Test.php @@ -0,0 +1,190 @@ +setUpEntitySchema( + [ + GH6531User::class, + GH6531Address::class, + GH6531Article::class, + GH6531ArticleAttribute::class, + GH6531Order::class, + GH6531OrderItem::class, + GH6531Product::class, + ] + ); + } + + /** + * @group 6531 + */ + public function testSimpleDerivedIdentity() : void + { + $user = new GH6531User(); + $address = new GH6531Address(); + $address->user = $user; + + $this->_em->persist($user); + $this->_em->persist($address); + $this->_em->flush(); + + self::assertSame($user, $this->_em->find(GH6531User::class, $user->id)); + self::assertSame($address, $this->_em->find(GH6531Address::class, $user)); + } + + /** + * @group 6531 + */ + public function testDynamicAttributes() : void + { + $article = new GH6531Article(); + $article->addAttribute('name', 'value'); + + $this->_em->persist($article); + $this->_em->flush(); + + self::assertSame( + $article->attributes['name'], + $this->_em->find(GH6531ArticleAttribute::class, ['article' => $article, 'attribute' => 'name']) + ); + } + + /** + * @group 6531 + */ + public function testJoinTableWithMetadata() : void + { + $product = new GH6531Product(); + $this->_em->persist($product); + $this->_em->flush(); + + $order = new GH6531Order(); + $order->addItem($product, 2); + + $this->_em->persist($order); + $this->_em->flush(); + + self::assertSame( + $order->items->first(), + $this->_em->find(GH6531OrderItem::class, ['product' => $product, 'order' => $order]) + ); + } +} + +/** + * @Entity + */ +class GH6531User +{ + /** @Id @Column(type="integer") @GeneratedValue */ + public $id; +} + +/** + * @Entity + */ +class GH6531Address +{ + /** @Id @OneToOne(targetEntity=GH6531User::class) */ + public $user; +} + +/** + * @Entity + */ +class GH6531Article +{ + /** @Id @Column(type="integer") @GeneratedValue */ + public $id; + + /** @OneToMany(targetEntity=GH6531ArticleAttribute::class, mappedBy="article", cascade={"ALL"}, indexBy="attribute") */ + public $attributes; + + public function addAttribute(string $name, string $value) + { + $this->attributes[$name] = new GH6531ArticleAttribute($name, $value, $this); + } +} + +/** + * @Entity + */ +class GH6531ArticleAttribute +{ + /** @Id @ManyToOne(targetEntity=GH6531Article::class, inversedBy="attributes") */ + public $article; + + /** @Id @Column(type="string") */ + public $attribute; + + /** @Column(type="string") */ + public $value; + + public function __construct(string $name, string $value, GH6531Article $article) + { + $this->attribute = $name; + $this->value = $value; + $this->article = $article; + } +} + +/** + * @Entity + */ +class GH6531Order +{ + /** @Id @Column(type="integer") @GeneratedValue */ + public $id; + + /** @OneToMany(targetEntity=GH6531OrderItem::class, mappedBy="order", cascade={"ALL"}) */ + public $items; + + public function __construct() + { + $this->items = new ArrayCollection(); + } + + public function addItem(GH6531Product $product, int $amount) : void + { + $this->items->add(new GH6531OrderItem($this, $product, $amount)); + } +} + +/** + * @Entity + */ +class GH6531Product +{ + /** @Id @Column(type="integer") @GeneratedValue */ + public $id; +} + +/** + * @Entity + */ +class GH6531OrderItem +{ + /** @Id @ManyToOne(targetEntity=GH6531Order::class) */ + public $order; + + /** @Id @ManyToOne(targetEntity=GH6531Product::class) */ + public $product; + + /** @Column(type="integer") */ + public $amount = 1; + + public function __construct(GH6531Order $order, GH6531Product $product, int $amount = 1) + { + $this->order = $order; + $this->product = $product; + $this->amount = $amount; + } +} From 35c3669ebc822c88444d92e9ffc739d12f551d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20FRAN=C3=87OIS?= Date: Mon, 22 Jan 2018 19:10:42 +0100 Subject: [PATCH 2/2] Fix handling entities with post generated IDs as FK This prevents a throw in UnitOfWork#addToIdentityMap because some fields are null. --- lib/Doctrine/ORM/UnitOfWork.php | 56 ++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 56017301b..6767e7ef3 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -940,7 +940,11 @@ class UnitOfWork implements PropertyChangedListener $class->setIdentifierValues($entity, $idValue); } - $this->entityIdentifiers[$oid] = $idValue; + // Some identifiers may be foreign keys to new entities. + // In this case, we don't have the value yet and should treat it as if we have a post-insert generator + if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) { + $this->entityIdentifiers[$oid] = $idValue; + } } $this->entityStates[$oid] = self::STATE_MANAGED; @@ -948,6 +952,20 @@ class UnitOfWork implements PropertyChangedListener $this->scheduleForInsert($entity); } + /** + * @param mixed[] $idValue + */ + private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool + { + foreach ($idValue as $idField => $idFieldValue) { + if ($idFieldValue === null && isset($class->associationMappings[$idField])) { + return true; + } + } + + return false; + } + /** * INTERNAL: * Computes the changeset of an individual entity, independently of the @@ -1033,12 +1051,16 @@ class UnitOfWork implements PropertyChangedListener $persister = $this->getEntityPersister($className); $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); + $insertionsForClass = []; + foreach ($this->entityInsertions as $oid => $entity) { if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { continue; } + $insertionsForClass[$oid] = $entity; + $persister->addInsert($entity); unset($this->entityInsertions[$oid]); @@ -1067,6 +1089,14 @@ class UnitOfWork implements PropertyChangedListener $this->addToIdentityMap($entity); } + } else { + foreach ($insertionsForClass as $oid => $entity) { + if (! isset($this->entityIdentifiers[$oid])) { + //entity was not added to identity map because some identifiers are foreign keys to new entities. + //add it now + $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity); + } + } } foreach ($entities as $entity) { @@ -1074,6 +1104,30 @@ class UnitOfWork implements PropertyChangedListener } } + /** + * @param object $entity + */ + private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void + { + $identifier = []; + + foreach ($class->getIdentifierFieldNames() as $idField) { + $value = $class->getFieldValue($entity, $idField); + + if (isset($class->associationMappings[$idField])) { + // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced. + $value = $this->getSingleIdentifierValue($value); + } + + $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value; + } + + $this->entityStates[$oid] = self::STATE_MANAGED; + $this->entityIdentifiers[$oid] = $identifier; + + $this->addToIdentityMap($entity); + } + /** * Executes all entity updates for entities of the specified type. *