From 6d11fc1caed827681245d4c0b76aa29b364481fe 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 1/6] UnitOfWork: Handle ids post-insert when some identifier fields are FKs and the target entity is new. This prevents a throw in UnitOfWork#addToIdentityMap because some fields are null. --- lib/Doctrine/ORM/UnitOfWork.php | 38 +++++++++++++- .../IdentityThroughForeignKeyTest/Product.php | 49 ++++++++++++++++++ .../ProductColor.php | 27 ++++++++++ .../ProductSize.php | 28 +++++++++++ .../ProductVariant.php | 36 +++++++++++++ .../IdentityThroughForeignKeyTest.php | 50 +++++++++++++++++++ 6 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/Product.php create mode 100644 tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductColor.php create mode 100644 tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductSize.php create mode 100644 tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductVariant.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/IdentityThroughForeignKeyTest.php diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 56017301bf2..00ec84164f0 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -939,8 +939,11 @@ private function persistNew($class, $entity) $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(!in_array(null, $idValue, true)) { + $this->entityIdentifiers[$oid] = $idValue; + } } $this->entityStates[$oid] = self::STATE_MANAGED; @@ -1033,12 +1036,16 @@ private function executeInserts($class) $persister = $this->getEntityPersister($className); $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); + $theseInsertions = []; + foreach ($this->entityInsertions as $oid => $entity) { if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { continue; } + $theseInsertions[$oid] = $entity; + $persister->addInsert($entity); unset($this->entityInsertions[$oid]); @@ -1067,6 +1074,33 @@ private function executeInserts($class) $this->addToIdentityMap($entity); } + } else { + foreach ($theseInsertions as $oid => $entity) { + if(!isset($this->entityIdentifiers[$oid])) { + //entity was not added to identity map because some identifiers ar foreign keys to new entities. + //add it now + $idFields = $class->getIdentifierFieldNames(); + foreach ($idFields 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] = $value; + } + + $this->entityIdentifiers[$oid] = $identifier; + + foreach($identifier as $idField => $idValue) { + $this->originalEntityData[$oid][$idField] = $idValue; + } + + $this->entityStates[$oid] = self::STATE_MANAGED; + $this->addToIdentityMap($entity); + } + } } foreach ($entities as $entity) { diff --git a/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/Product.php b/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/Product.php new file mode 100644 index 00000000000..d9a38a49938 --- /dev/null +++ b/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/Product.php @@ -0,0 +1,49 @@ +colors = new ArrayCollection(); + $this->sizes = new ArrayCollection(); + $this->variants = new ArrayCollection(); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductColor.php b/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductColor.php new file mode 100644 index 00000000000..0f1b85b152e --- /dev/null +++ b/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductColor.php @@ -0,0 +1,27 @@ +em = $this->_getTestEntityManager(); + } + + + public function testIdentityThroughForeignKeyCollectionPersistence() { + $product = new Product(); + + $color = new ProductColor(); + $color->product = $product; + $product->colors->add($color); + + $size = new ProductSize(); + $size->product = $product; + $product->sizes->add($size); + + $variant = new ProductVariant(); + $variant->product = $product; + $variant->color = $color; + $variant->size = $size; + $product->variants->add($variant); + + $this->em->persist($product); + $this->em->flush(); + + /** @var ConnectionMock $conn */ + $conn = $this->em->getConnection(); + print_r($conn->getInserts()); + $this->assertTrue(true); + } +} \ No newline at end of file From d1500e288644862efaf52eb3c5d7976062433a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20FRAN=C3=87OIS?= Date: Tue, 23 Jan 2018 10:33:26 +0100 Subject: [PATCH 2/6] Asserts un unititest --- .../Functional/IdentityThroughForeignKeyTest.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/IdentityThroughForeignKeyTest.php b/tests/Doctrine/Tests/ORM/Functional/IdentityThroughForeignKeyTest.php index 3077d79b5d9..6d361626ea7 100644 --- a/tests/Doctrine/Tests/ORM/Functional/IdentityThroughForeignKeyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/IdentityThroughForeignKeyTest.php @@ -40,11 +40,18 @@ public function testIdentityThroughForeignKeyCollectionPersistence() { $product->variants->add($variant); $this->em->persist($product); + + //check that flush does not throw $this->em->flush(); - /** @var ConnectionMock $conn */ - $conn = $this->em->getConnection(); - print_r($conn->getInserts()); - $this->assertTrue(true); + $identifier = $this->em->getClassMetadata(ProductVariant::class)->getIdentifierValues($variant); + + foreach($identifier as $k => $v) { + $identifier[$k] = $this->em->getClassMetadata(get_class($v))->getIdentifierValues($v)['id']; + } + + //check that identityMap content is correct + //find by primary key should return the same instance + $this->assertTrue($variant === $this->em->find(ProductVariant::class, $identifier)); } } \ No newline at end of file From 716ec1a9a0796568d2afc7894d72c66eca346be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20FRAN=C3=87OIS?= Date: Tue, 23 Jan 2018 12:00:05 +0100 Subject: [PATCH 3/6] Delay id map handling only if the missing id(s) correspond to a mapping --- lib/Doctrine/ORM/UnitOfWork.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 00ec84164f0..96c848cd0b3 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -939,9 +939,17 @@ private function persistNew($class, $entity) $class->setIdentifierValues($entity, $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(!in_array(null, $idValue, true)) { + $hasMissingIdsWhichAreForeignKeys = false; + foreach($idValue as $idField => $idFieldValue) { + if($idFieldValue === null && isset($class->associationMappings[$idField])) { + $hasMissingIdsWhichAreForeignKeys = true; + break; + } + } + if(!$hasMissingIdsWhichAreForeignKeys) { $this->entityIdentifiers[$oid] = $idValue; } } @@ -1077,7 +1085,7 @@ private function executeInserts($class) } else { foreach ($theseInsertions as $oid => $entity) { if(!isset($this->entityIdentifiers[$oid])) { - //entity was not added to identity map because some identifiers ar foreign keys to new entities. + //entity was not added to identity map because some identifiers are foreign keys to new entities. //add it now $idFields = $class->getIdentifierFieldNames(); foreach ($idFields as $idField) { From ec1edd42b6b6c0478e37e11940b0e715397e5582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20FRAN=C3=87OIS?= Date: Tue, 23 Jan 2018 12:35:02 +0100 Subject: [PATCH 4/6] Make travis happy again --- lib/Doctrine/ORM/UnitOfWork.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 96c848cd0b3..35a4ade5673 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1087,6 +1087,7 @@ private function executeInserts($class) if(!isset($this->entityIdentifiers[$oid])) { //entity was not added to identity map because some identifiers are foreign keys to new entities. //add it now + $identifier = []; $idFields = $class->getIdentifierFieldNames(); foreach ($idFields as $idField) { $value = $class->getFieldValue($entity, $idField); From cbab976b6851b82d0db1f98743ddab5c35ef896f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20FRAN=C3=87OIS?= Date: Tue, 23 Jan 2018 13:09:01 +0100 Subject: [PATCH 5/6] phpcbf --- .../IdentityThroughForeignKeyTest/Product.php | 13 +++++----- .../ProductColor.php | 5 ++-- .../ProductSize.php | 6 +++-- .../ProductVariant.php | 5 ++-- .../IdentityThroughForeignKeyTest.php | 24 ++++++++++--------- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/Product.php b/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/Product.php index d9a38a49938..f01c9bbe51b 100644 --- a/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/Product.php +++ b/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/Product.php @@ -2,7 +2,6 @@ namespace Doctrine\Tests\Models\IdentityThroughForeignKeyTest; - use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -13,7 +12,8 @@ * @Entity() * @Table(name="identitythroughforeignkey_product") */ -class Product { +class Product +{ /** * @var integer @@ -41,9 +41,10 @@ class Product { */ public $variants; - public function __construct() { - $this->colors = new ArrayCollection(); - $this->sizes = new ArrayCollection(); + public function __construct() + { + $this->colors = new ArrayCollection(); + $this->sizes = new ArrayCollection(); $this->variants = new ArrayCollection(); } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductColor.php b/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductColor.php index 0f1b85b152e..74476404f1a 100644 --- a/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductColor.php +++ b/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductColor.php @@ -8,7 +8,8 @@ * @Entity() * @Table(name="identitythroughforeignkey_product_color") */ -class ProductColor { +class ProductColor +{ /** * @var integer @@ -24,4 +25,4 @@ class ProductColor { * @JoinColumn(name="product_id", referencedColumnName="id") */ public $product; -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductSize.php b/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductSize.php index 480280d7e4f..b3638fe8f86 100644 --- a/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductSize.php +++ b/tests/Doctrine/Tests/Models/IdentityThroughForeignKeyTest/ProductSize.php @@ -1,6 +1,7 @@ em = $this->_getTestEntityManager(); } - public function testIdentityThroughForeignKeyCollectionPersistence() { + public function testIdentityThroughForeignKeyCollectionPersistence() + { $product = new Product(); - $color = new ProductColor(); + $color = new ProductColor(); $color->product = $product; $product->colors->add($color); - $size = new ProductSize(); + $size = new ProductSize(); $size->product = $product; $product->sizes->add($size); - $variant = new ProductVariant(); + $variant = new ProductVariant(); $variant->product = $product; - $variant->color = $color; - $variant->size = $size; + $variant->color = $color; + $variant->size = $size; $product->variants->add($variant); $this->em->persist($product); @@ -46,7 +48,7 @@ public function testIdentityThroughForeignKeyCollectionPersistence() { $identifier = $this->em->getClassMetadata(ProductVariant::class)->getIdentifierValues($variant); - foreach($identifier as $k => $v) { + foreach ($identifier as $k => $v) { $identifier[$k] = $this->em->getClassMetadata(get_class($v))->getIdentifierValues($v)['id']; } @@ -54,4 +56,4 @@ public function testIdentityThroughForeignKeyCollectionPersistence() { //find by primary key should return the same instance $this->assertTrue($variant === $this->em->find(ProductVariant::class, $identifier)); } -} \ No newline at end of file +} From 6608238234a3782e1fdf45d5ccac7b9e3e3c5172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20FRAN=C3=87OIS?= Date: Fri, 26 Jan 2018 16:18:37 +0100 Subject: [PATCH 6/6] refactor, for the sake of clarity --- lib/Doctrine/ORM/UnitOfWork.php | 73 ++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 35a4ade5673..3dea69f247c 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -942,14 +942,7 @@ private function persistNew($class, $entity) // 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 - $hasMissingIdsWhichAreForeignKeys = false; - foreach($idValue as $idField => $idFieldValue) { - if($idFieldValue === null && isset($class->associationMappings[$idField])) { - $hasMissingIdsWhichAreForeignKeys = true; - break; - } - } - if(!$hasMissingIdsWhichAreForeignKeys) { + if(!$this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) { $this->entityIdentifiers[$oid] = $idValue; } } @@ -959,6 +952,18 @@ private function persistNew($class, $entity) $this->scheduleForInsert($entity); } + private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) { + $hasMissingIdsWhichAreForeignKeys = false; + foreach($idValue as $idField => $idFieldValue) { + if($idFieldValue === null && isset($class->associationMappings[$idField])) { + $hasMissingIdsWhichAreForeignKeys = true; + break; + } + } + + return$hasMissingIdsWhichAreForeignKeys; + } + /** * INTERNAL: * Computes the changeset of an individual entity, independently of the @@ -1044,7 +1049,7 @@ private function executeInserts($class) $persister = $this->getEntityPersister($className); $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); - $theseInsertions = []; + $insertionsForClass = []; foreach ($this->entityInsertions as $oid => $entity) { @@ -1052,7 +1057,7 @@ private function executeInserts($class) continue; } - $theseInsertions[$oid] = $entity; + $insertionsForClass[$oid] = $entity; $persister->addInsert($entity); @@ -1083,31 +1088,11 @@ private function executeInserts($class) $this->addToIdentityMap($entity); } } else { - foreach ($theseInsertions as $oid => $entity) { + 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 - $identifier = []; - $idFields = $class->getIdentifierFieldNames(); - foreach ($idFields 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] = $value; - } - - $this->entityIdentifiers[$oid] = $identifier; - - foreach($identifier as $idField => $idValue) { - $this->originalEntityData[$oid][$idField] = $idValue; - } - - $this->entityStates[$oid] = self::STATE_MANAGED; - $this->addToIdentityMap($entity); + $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity); } } } @@ -1117,6 +1102,30 @@ private function executeInserts($class) } } + private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, $oid, $entity) { + $identifier = []; + $idFields = $class->getIdentifierFieldNames(); + foreach ($idFields 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] = $value; + } + + $this->entityIdentifiers[$oid] = $identifier; + + foreach($identifier as $idField => $idValue) { + $this->originalEntityData[$oid][$idField] = $idValue; + } + + $this->entityStates[$oid] = self::STATE_MANAGED; + $this->addToIdentityMap($entity); + } + /** * Executes all entity updates for entities of the specified type. *