Skip to content

Commit 58df407

Browse files
committed
Merge branch '2.16.x' into 2.17.x
* 2.16.x: Turn identity map collisions from exception to deprecation notice (#10878) Add possibility to set reportFieldsWhereDeclared to true in ORMSetup (#10865) Fix UnitOfWork->originalEntityData is missing not-modified collections after computeChangeSet (#9301) Add an UPGRADE notice about the potential changes in commit order (#10866) Update branch metadata (#10862)
2 parents eda1909 + a616914 commit 58df407

File tree

12 files changed

+274
-17
lines changed

12 files changed

+274
-17
lines changed

.doctrine-project.json

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,27 @@
1212
"upcoming": true
1313
},
1414
{
15-
"name": "2.16",
16-
"branchName": "2.16.x",
17-
"slug": "2.16",
15+
"name": "2.17",
16+
"branchName": "2.17.x",
17+
"slug": "2.17",
1818
"upcoming": true
1919
},
2020
{
21-
"name": "2.15",
22-
"branchName": "2.15.x",
23-
"slug": "2.15",
21+
"name": "2.16",
22+
"branchName": "2.16.x",
23+
"slug": "2.16",
2424
"current": true,
2525
"aliases": [
2626
"current",
2727
"stable"
2828
]
2929
},
30+
{
31+
"name": "2.15",
32+
"branchName": "2.15.x",
33+
"slug": "2.15",
34+
"maintained": false
35+
},
3036
{
3137
"name": "2.14",
3238
"branchName": "2.14.x",

UPGRADE.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
# Upgrade to 2.16
22

3+
## Deprecated accepting duplicate IDs in the identity map
4+
5+
For any given entity class and ID value, there should be only one object instance
6+
representing the entity.
7+
8+
In https://github.com/doctrine/orm/pull/10785, a check was added that will guard this
9+
in the identity map. The most probable cause for violations of this rule are collisions
10+
of application-provided IDs.
11+
12+
In ORM 2.16.0, the check was added by throwing an exception. In ORM 2.16.1, this will be
13+
changed to a deprecation notice. ORM 3.0 will make it an exception again. Use
14+
`\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap()` if you want to opt-in
15+
to the new mode.
16+
17+
## Potential changes to the order in which `INSERT`s are executed
18+
19+
In https://github.com/doctrine/orm/pull/10547, the commit order computation was improved
20+
to fix a series of bugs where a correct (working) commit order was previously not found.
21+
Also, the new computation may get away with fewer queries being executed: By inserting
22+
referred-to entities first and using their ID values for foreign key fields in subsequent
23+
`INSERT` statements, additional `UPDATE` statements that were previously necessary can be
24+
avoided.
25+
26+
When using database-provided, auto-incrementing IDs, this may lead to IDs being assigned
27+
to entities in a different order than it was previously the case.
28+
329
## Deprecated `\Doctrine\ORM\Internal\CommitOrderCalculator` and related classes
430

531
With changes made to the commit order computation, the internal classes

docs/en/reference/advanced-configuration.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ steps of configuration.
2929
3030
$config = new Configuration;
3131
$config->setMetadataCache($metadataCache);
32-
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
32+
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
3333
$config->setMetadataDriverImpl($driverImpl);
3434
$config->setQueryCache($queryCache);
3535
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
@@ -134,7 +134,7 @@ The attribute driver can be injected in the ``Doctrine\ORM\Configuration``:
134134
<?php
135135
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
136136
137-
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
137+
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
138138
$config->setMetadataDriverImpl($driverImpl);
139139
140140
The path information to the entities is required for the attribute

lib/Doctrine/ORM/Configuration.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,4 +1117,14 @@ public function setLazyGhostObjectEnabled(bool $flag): void
11171117

11181118
$this->_attributes['isLazyGhostObjectEnabled'] = $flag;
11191119
}
1120+
1121+
public function setRejectIdCollisionInIdentityMap(bool $flag): void
1122+
{
1123+
$this->_attributes['rejectIdCollisionInIdentityMap'] = $flag;
1124+
}
1125+
1126+
public function isRejectIdCollisionInIdentityMapEnabled(): bool
1127+
{
1128+
return $this->_attributes['rejectIdCollisionInIdentityMap'] ?? false;
1129+
}
11201130
}

lib/Doctrine/ORM/ORMSetup.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ public static function createAttributeMetadataConfiguration(
101101
array $paths,
102102
bool $isDevMode = false,
103103
?string $proxyDir = null,
104-
?CacheItemPoolInterface $cache = null
104+
?CacheItemPoolInterface $cache = null,
105+
bool $reportFieldsWhereDeclared = false
105106
): Configuration {
106107
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
107-
$config->setMetadataDriverImpl(new AttributeDriver($paths));
108+
$config->setMetadataDriverImpl(new AttributeDriver($paths, $reportFieldsWhereDeclared));
108109

109110
return $config;
110111
}

lib/Doctrine/ORM/UnitOfWork.php

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,7 @@ public function computeChangeSet(ClassMetadata $class, $entity)
692692
if ($class->isCollectionValuedAssociation($name) && $value !== null) {
693693
if ($value instanceof PersistentCollection) {
694694
if ($value->getOwner() === $entity) {
695+
$actualData[$name] = $value;
695696
continue;
696697
}
697698

@@ -1634,8 +1635,10 @@ public function addToIdentityMap($entity)
16341635

16351636
if (isset($this->identityMap[$className][$idHash])) {
16361637
if ($this->identityMap[$className][$idHash] !== $entity) {
1637-
throw new RuntimeException(sprintf(
1638-
<<<'EXCEPTION'
1638+
if ($this->em->getConfiguration()->isRejectIdCollisionInIdentityMapEnabled()) {
1639+
throw new RuntimeException(
1640+
sprintf(
1641+
<<<'EXCEPTION'
16391642
While adding an entity of class %s with an ID hash of "%s" to the identity map,
16401643
another object of class %s was already present for the same ID. This exception
16411644
is a safeguard against an internal inconsistency - IDs should uniquely map to
@@ -1650,11 +1653,42 @@ public function addToIdentityMap($entity)
16501653
16511654
Otherwise, it might be an ORM-internal inconsistency, please report it.
16521655
EXCEPTION
1653-
,
1654-
get_class($entity),
1655-
$idHash,
1656-
get_class($this->identityMap[$className][$idHash])
1657-
));
1656+
,
1657+
get_class($entity),
1658+
$idHash,
1659+
get_class($this->identityMap[$className][$idHash])
1660+
)
1661+
);
1662+
} else {
1663+
Deprecation::trigger(
1664+
'doctrine/orm',
1665+
'https://github.com/doctrine/orm/pull/10785',
1666+
<<<'EXCEPTION'
1667+
While adding an entity of class %s with an ID hash of "%s" to the identity map,
1668+
another object of class %s was already present for the same ID. This will trigger
1669+
an exception in ORM 3.0.
1670+
1671+
IDs should uniquely map to entity object instances. This problem may occur if:
1672+
1673+
- you use application-provided IDs and reuse ID values;
1674+
- database-provided IDs are reassigned after truncating the database without
1675+
clearing the EntityManager;
1676+
- you might have been using EntityManager#getReference() to create a reference
1677+
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
1678+
entity.
1679+
1680+
Otherwise, it might be an ORM-internal inconsistency, please report it.
1681+
1682+
To opt-in to the new exception, call
1683+
\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap on the entity
1684+
manager's configuration.
1685+
EXCEPTION
1686+
,
1687+
get_class($entity),
1688+
$idHash,
1689+
get_class($this->identityMap[$className][$idHash])
1690+
);
1691+
}
16581692
}
16591693

16601694
return false;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\Issue9300;
6+
7+
use Doctrine\Common\Collections\ArrayCollection;
8+
use Doctrine\Common\Collections\Collection;
9+
use Doctrine\ORM\Mapping\Column;
10+
use Doctrine\ORM\Mapping\Entity;
11+
use Doctrine\ORM\Mapping\GeneratedValue;
12+
use Doctrine\ORM\Mapping\Id;
13+
use Doctrine\ORM\Mapping\ManyToMany;
14+
15+
/**
16+
* @Entity
17+
*/
18+
class Issue9300Child
19+
{
20+
/**
21+
* @var int
22+
* @Id
23+
* @Column(type="integer")
24+
* @GeneratedValue
25+
*/
26+
public $id;
27+
28+
/**
29+
* @var Collection<int, Issue9300Parent>
30+
* @ManyToMany(targetEntity="Issue9300Parent")
31+
*/
32+
public $parents;
33+
34+
/**
35+
* @var string
36+
* @Column(type="string")
37+
*/
38+
public $name;
39+
40+
public function __construct()
41+
{
42+
$this->parents = new ArrayCollection();
43+
}
44+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\Issue9300;
6+
7+
use Doctrine\ORM\Mapping\Column;
8+
use Doctrine\ORM\Mapping\Entity;
9+
use Doctrine\ORM\Mapping\GeneratedValue;
10+
use Doctrine\ORM\Mapping\Id;
11+
12+
/**
13+
* @Entity
14+
*/
15+
class Issue9300Parent
16+
{
17+
/**
18+
* @var int
19+
* @Id
20+
* @Column(type="integer")
21+
* @GeneratedValue
22+
*/
23+
public $id;
24+
25+
/**
26+
* @var string
27+
* @Column(type="string")
28+
*/
29+
public $name;
30+
}

tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,8 @@ public function testWrongAssociationInstance(): void
13291329

13301330
public function testItThrowsWhenReferenceUsesIdAssignedByDatabase(): void
13311331
{
1332+
$this->_em->getConfiguration()->setRejectIdCollisionInIdentityMap(true);
1333+
13321334
$user = new CmsUser();
13331335
$user->name = 'test';
13341336
$user->username = 'test';
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional\Ticket;
6+
7+
use Doctrine\Common\Collections\ArrayCollection;
8+
use Doctrine\Tests\Models\Issue9300\Issue9300Child;
9+
use Doctrine\Tests\Models\Issue9300\Issue9300Parent;
10+
use Doctrine\Tests\OrmFunctionalTestCase;
11+
12+
/**
13+
* @group GH-9300
14+
*/
15+
class Issue9300Test extends OrmFunctionalTestCase
16+
{
17+
protected function setUp(): void
18+
{
19+
$this->useModelSet('issue9300');
20+
21+
parent::setUp();
22+
}
23+
24+
/**
25+
* @group GH-9300
26+
*/
27+
public function testPersistedCollectionIsPresentInOriginalDataAfterFlush(): void
28+
{
29+
$parent = new Issue9300Parent();
30+
$child = new Issue9300Child();
31+
$child->parents->add($parent);
32+
33+
$parent->name = 'abc';
34+
$child->name = 'abc';
35+
36+
$this->_em->persist($parent);
37+
$this->_em->persist($child);
38+
$this->_em->flush();
39+
40+
$parent->name = 'abcd';
41+
$child->name = 'abcd';
42+
43+
$this->_em->flush();
44+
45+
self::assertArrayHasKey('parents', $this->_em->getUnitOfWork()->getOriginalEntityData($child));
46+
}
47+
48+
/**
49+
* @group GH-9300
50+
*/
51+
public function testPersistingCollectionAfterFlushWorksAsExpected(): void
52+
{
53+
$parentOne = new Issue9300Parent();
54+
$parentTwo = new Issue9300Parent();
55+
$childOne = new Issue9300Child();
56+
57+
$parentOne->name = 'abc';
58+
$parentTwo->name = 'abc';
59+
$childOne->name = 'abc';
60+
$childOne->parents = new ArrayCollection([$parentOne]);
61+
62+
$this->_em->persist($parentOne);
63+
$this->_em->persist($parentTwo);
64+
$this->_em->persist($childOne);
65+
$this->_em->flush();
66+
67+
// Recalculate change-set -> new original data
68+
$childOne->name = 'abcd';
69+
$this->_em->flush();
70+
71+
$childOne->parents = new ArrayCollection([$parentTwo]);
72+
73+
$this->_em->flush();
74+
$this->_em->clear();
75+
76+
$childOneFresh = $this->_em->find(Issue9300Child::class, $childOne->id);
77+
self::assertCount(1, $childOneFresh->parents);
78+
self::assertEquals($parentTwo->id, $childOneFresh->parents[0]->id);
79+
}
80+
}

0 commit comments

Comments
 (0)