Compare commits

...

10 Commits

Author SHA1 Message Date
Alexander M. Turek 597a63a86c PHPStan 1.10.28, Psalm 5.14.1 (#10895) 2023-08-09 15:05:08 +02:00
Bei Xiao 6b220e3c90 Fix return type of getSingleScalarResult (#10870) 2023-08-09 11:42:00 +02:00
Matthias Pigulla 6de4b68705 Use a dedicated exception for the check added in #10785 (#10881)
This adds a dedicated exception for the case that objects with colliding identities are to be put into the identity map.

Implements #10872.
2023-08-09 11:38:35 +02:00
Matthias Pigulla 16c0151831 Document more clearly that the insert order is an implementation detail (#10883) 2023-08-09 11:36:05 +02:00
Matthias Pigulla 440b244ebc Fix broken changeset computation for entities loaded through fetch=EAGER + using inheritance (#10884)
#10880 reports a case where the changes from #10785 cause entity updates to be missed.

Upon closer inspection, this change seems to be causing it:

https://github.com/doctrine/orm/pull/10785/files#diff-55a900494fc8033ab498c53929716caf0aa39d6bdd7058e7d256787a24412ee4L2990-L3003

The code was changed to use `registerManaged()` instead, which basically does the same things, but (since #10785) also includes an additional check against duplicate entity instances.

But, one detail slipped through tests and reviews: `registerManaged()` also updates `\Doctrine\ORM\UnitOfWork::$originalEntityData`, which is used to compute entity changesets. An empty array `[]` was passed for $data here.

This will make the changeset computation assume that a partial object was loaded and effectively ignore all field updates here:

https://github.com/doctrine/orm/blob/a616914887ea160db4158d2c67752e99624f7c8a/lib/Doctrine/ORM/UnitOfWork.php#L762-L764

I think that, effectively, it is sufficient to call `registerManaged()` only in the two cases where a proxy was created.

Calling `registerManaged()` with `[]` as data for a proxy object is consistent with e. g. `\Doctrine\ORM\EntityManager::getReference()`.

In the case that a full entity has to be loaded, we need not call `registerManaged()` at all, since that will already happen inside `EntityManager::find()` (or, more specifically, `UnitOfWork::createEntity()` called inside it).

Note that the test case has to make some provisions so that we actually reach this case:
* Load an entity that uses `fetch="EAGER"` on a to-one association
* That association being against a class that uses inheritance (why's that?)
2023-08-09 11:34:53 +02:00
Matthias Pigulla a616914887 Turn identity map collisions from exception to deprecation notice (#10878)
In #10785, a check was added that prevents entity instances from getting into the identity map when another object for the same ID is already being tracked.

This caused regressions for users that work with application-provided IDs and expect this condition to fail with `UniqueConstraintViolationExceptions` when flushing to the database.

Thus, this PR turns the exception into a deprecation notice. Users can opt-in to the new behavior. In 3.0, the exception will be used.

Implements #10871.
2023-08-04 14:06:02 +02:00
Dieter Beck fd0bdc69b0 Add possibility to set reportFieldsWhereDeclared to true in ORMSetup (#10865)
Otherwise it is impossible to avoid a deprecation warning when using ORMSetup::createAttributeMetadataConfiguration()
2023-08-02 14:34:13 +02:00
Michael Olšavský f50803ccb9 Fix UnitOfWork->originalEntityData is missing not-modified collections after computeChangeSet (#9301)
* Fix original data incomplete after flush

* Apply suggestions from code review

Co-authored-by: Alexander M. Turek <me@derrabus.de>

---------

Co-authored-by: Alexander M. Turek <me@derrabus.de>
2023-08-02 13:44:15 +02:00
Matthias Pigulla eeefc6bc0f Add an UPGRADE notice about the potential changes in commit order (#10866) 2023-08-02 13:42:49 +02:00
Grégoire Paris 710dde83aa Update branch metadata (#10862) 2023-08-01 14:56:34 +02:00
22 changed files with 446 additions and 46 deletions
+12 -6
View File
@@ -12,21 +12,27 @@
"upcoming": true
},
{
"name": "2.16",
"branchName": "2.16.x",
"slug": "2.16",
"name": "2.17",
"branchName": "2.17.x",
"slug": "2.17",
"upcoming": true
},
{
"name": "2.15",
"branchName": "2.15.x",
"slug": "2.15",
"name": "2.16",
"branchName": "2.16.x",
"slug": "2.16",
"current": true,
"aliases": [
"current",
"stable"
]
},
{
"name": "2.15",
"branchName": "2.15.x",
"slug": "2.15",
"maintained": false
},
{
"name": "2.14",
"branchName": "2.14.x",
+26
View File
@@ -1,5 +1,31 @@
# Upgrade to 2.16
## Deprecated accepting duplicate IDs in the identity map
For any given entity class and ID value, there should be only one object instance
representing the entity.
In https://github.com/doctrine/orm/pull/10785, a check was added that will guard this
in the identity map. The most probable cause for violations of this rule are collisions
of application-provided IDs.
In ORM 2.16.0, the check was added by throwing an exception. In ORM 2.16.1, this will be
changed to a deprecation notice. ORM 3.0 will make it an exception again. Use
`\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap()` if you want to opt-in
to the new mode.
## Potential changes to the order in which `INSERT`s are executed
In https://github.com/doctrine/orm/pull/10547, the commit order computation was improved
to fix a series of bugs where a correct (working) commit order was previously not found.
Also, the new computation may get away with fewer queries being executed: By inserting
referred-to entities first and using their ID values for foreign key fields in subsequent
`INSERT` statements, additional `UPDATE` statements that were previously necessary can be
avoided.
When using database-provided, auto-incrementing IDs, this may lead to IDs being assigned
to entities in a different order than it was previously the case.
## Deprecated `\Doctrine\ORM\Internal\CommitOrderCalculator` and related classes
With changes made to the commit order computation, the internal classes
+2 -2
View File
@@ -42,14 +42,14 @@
"doctrine/annotations": "^1.13 || ^2",
"doctrine/coding-standard": "^9.0.2 || ^12.0",
"phpbench/phpbench": "^0.16.10 || ^1.0",
"phpstan/phpstan": "~1.4.10 || 1.10.25",
"phpstan/phpstan": "~1.4.10 || 1.10.28",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^4.4 || ^5.4 || ^6.0",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2",
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"vimeo/psalm": "4.30.0 || 5.13.1"
"vimeo/psalm": "4.30.0 || 5.14.1"
},
"conflict": {
"doctrine/annotations": "<1.13 || >= 3.0"
+2 -2
View File
@@ -29,7 +29,7 @@ steps of configuration.
$config = new Configuration;
$config->setMetadataCache($metadataCache);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$config->setMetadataDriverImpl($driverImpl);
$config->setQueryCache($queryCache);
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
@@ -134,7 +134,7 @@ The attribute driver can be injected in the ``Doctrine\ORM\Configuration``:
<?php
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$config->setMetadataDriverImpl($driverImpl);
The path information to the entities is required for the attribute
@@ -192,6 +192,11 @@ be properly synchronized with the database when
database in the most efficient way and a single, short transaction,
taking care of maintaining referential integrity.
.. note::
Do not make any assumptions in your code about the number of queries
it takes to flush changes, about the ordering of ``INSERT``, ``UPDATE``
and ``DELETE`` queries or the order in which entities will be processed.
Example:
+1 -2
View File
@@ -1010,9 +1010,8 @@ abstract class AbstractQuery
*
* Alias for getSingleResult(HYDRATE_SINGLE_SCALAR).
*
* @return bool|float|int|string The scalar result.
* @return bool|float|int|string|null The scalar result.
*
* @throws NoResultException If the query returned no result.
* @throws NonUniqueResultException If the query result is not unique.
*/
public function getSingleScalarResult()
+10
View File
@@ -1117,4 +1117,14 @@ class Configuration extends \Doctrine\DBAL\Configuration
$this->_attributes['isLazyGhostObjectEnabled'] = $flag;
}
public function setRejectIdCollisionInIdentityMap(bool $flag): void
{
$this->_attributes['rejectIdCollisionInIdentityMap'] = $flag;
}
public function isRejectIdCollisionInIdentityMapEnabled(): bool
{
return $this->_attributes['rejectIdCollisionInIdentityMap'] ?? false;
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function get_class;
use function sprintf;
final class EntityIdentityCollisionException extends ORMException
{
/**
* @param object $existingEntity
* @param object $newEntity
*/
public static function create($existingEntity, $newEntity, string $idHash): self
{
return new self(
sprintf(
<<<'EXCEPTION'
While adding an entity of class %s with an ID hash of "%s" to the identity map,
another object of class %s was already present for the same ID. This exception
is a safeguard against an internal inconsistency - IDs should uniquely map to
entity object instances. This problem may occur if:
- you use application-provided IDs and reuse ID values;
- database-provided IDs are reassigned after truncating the database without
clearing the EntityManager;
- you might have been using EntityManager#getReference() to create a reference
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
entity.
Otherwise, it might be an ORM-internal inconsistency, please report it.
EXCEPTION
,
get_class($newEntity),
$idHash,
get_class($existingEntity)
)
);
}
}
@@ -944,7 +944,10 @@ class XmlDriver extends FileDriver
private function getCascadeMappings(SimpleXMLElement $cascadeElement): array
{
$cascades = [];
foreach ($cascadeElement->children() as $action) {
$children = $cascadeElement->children();
assert($children !== null);
foreach ($children as $action) {
// According to the JPA specifications, XML uses "cascade-persist"
// instead of "persist". Here, both variations
// are supported because YAML, Annotation and Attribute use "persist"
@@ -30,7 +30,10 @@ class UnderscoreNamingStrategy implements NamingStrategy
/** @var int */
private $case;
/** @var string */
/**
* @var string
* @psalm-var non-empty-string
*/
private $pattern;
/**
+3 -2
View File
@@ -101,10 +101,11 @@ final class ORMSetup
array $paths,
bool $isDevMode = false,
?string $proxyDir = null,
?CacheItemPoolInterface $cache = null
?CacheItemPoolInterface $cache = null,
bool $reportFieldsWhereDeclared = false
): Configuration {
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
$config->setMetadataDriverImpl(new AttributeDriver($paths));
$config->setMetadataDriverImpl(new AttributeDriver($paths, $reportFieldsWhereDeclared));
return $config;
}
+2 -2
View File
@@ -1376,7 +1376,7 @@ public function __construct(<params>)
$this->staticReflection[$metadata->name]['methods'][] = strtolower($methodName);
$var = sprintf('%sMethodTemplate', $type);
$template = static::$$var;
$template = (string) static::$$var;
$methodTypeHint = '';
$types = Type::getTypesMap();
@@ -1695,7 +1695,7 @@ public function __construct(<params>)
}
if (isset($fieldMapping['options']['comment']) && $fieldMapping['options']['comment']) {
$options[] = '"comment"="' . str_replace('"', '""', $fieldMapping['options']['comment']) . '"';
$options[] = '"comment"="' . str_replace('"', '""', (string) $fieldMapping['options']['comment']) . '"';
}
if (isset($fieldMapping['options']['collation']) && $fieldMapping['options']['collation']) {
@@ -404,7 +404,7 @@ class LimitSubqueryOutputWalker extends SqlWalker
/**
* @return string[][]
* @psalm-return array{0: list<string>, 1: list<string>}
* @psalm-return array{0: list<non-empty-string>, 1: list<string>}
*/
private function generateSqlAliasReplacements(): array
{
+27 -18
View File
@@ -23,6 +23,7 @@ use Doctrine\ORM\Event\PreFlushEventArgs;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Event\PreRemoveEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Exception\EntityIdentityCollisionException;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Exception\UnexpectedAssociationValue;
use Doctrine\ORM\Id\AssignedGenerator;
@@ -692,6 +693,7 @@ class UnitOfWork implements PropertyChangedListener
if ($class->isCollectionValuedAssociation($name) && $value !== null) {
if ($value instanceof PersistentCollection) {
if ($value->getOwner() === $entity) {
$actualData[$name] = $value;
continue;
}
@@ -1623,6 +1625,7 @@ class UnitOfWork implements PropertyChangedListener
* the entity in question is already managed.
*
* @throws ORMInvalidArgumentException
* @throws EntityIdentityCollisionException
*
* @ignore
*/
@@ -1634,27 +1637,38 @@ class UnitOfWork implements PropertyChangedListener
if (isset($this->identityMap[$className][$idHash])) {
if ($this->identityMap[$className][$idHash] !== $entity) {
throw new RuntimeException(sprintf(
if ($this->em->getConfiguration()->isRejectIdCollisionInIdentityMapEnabled()) {
throw EntityIdentityCollisionException::create($this->identityMap[$className][$idHash], $entity, $idHash);
}
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10785',
<<<'EXCEPTION'
While adding an entity of class %s with an ID hash of "%s" to the identity map,
another object of class %s was already present for the same ID. This exception
is a safeguard against an internal inconsistency - IDs should uniquely map to
entity object instances. This problem may occur if:
another object of class %s was already present for the same ID. This will trigger
an exception in ORM 3.0.
IDs should uniquely map to entity object instances. This problem may occur if:
- you use application-provided IDs and reuse ID values;
- database-provided IDs are reassigned after truncating the database without
clearing the EntityManager;
- you might have been using EntityManager#getReference() to create a reference
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
entity.
- database-provided IDs are reassigned after truncating the database without
clearing the EntityManager;
- you might have been using EntityManager#getReference() to create a reference
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
entity.
Otherwise, it might be an ORM-internal inconsistency, please report it.
Otherwise, it might be an ORM-internal inconsistency, please report it.
To opt-in to the new exception, call
\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap on the entity
manager's configuration.
EXCEPTION
,
get_class($entity),
$idHash,
get_class($this->identityMap[$className][$idHash])
));
);
}
return false;
@@ -3014,6 +3028,7 @@ EXCEPTION
// We are negating the condition here. Other cases will assume it is valid!
case $hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER:
$newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId);
$this->registerManaged($newValue, $associatedId, []);
break;
// Deferred eager load only works for single identifier classes
@@ -3022,6 +3037,7 @@ EXCEPTION
$this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($normalizedAssociatedId);
$newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId);
$this->registerManaged($newValue, $associatedId, []);
break;
default:
@@ -3029,13 +3045,6 @@ EXCEPTION
$newValue = $this->em->find($assoc['targetEntity'], $normalizedAssociatedId);
break;
}
if ($newValue === null) {
break;
}
$this->registerManaged($newValue, $associatedId, []);
break;
}
$this->originalEntityData[$oid][$field] = $newValue;
+1 -4
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.13.1@086b94371304750d1c673315321a55d15fc59015">
<files psalm-version="5.14.1@b9d355e0829c397b9b3b47d0c0ed042a8a70284d">
<file src="lib/Doctrine/ORM/AbstractQuery.php">
<DeprecatedClass>
<code>IterableResult</code>
@@ -964,9 +964,6 @@
<code><![CDATA[$joinColumnElement['options']->children()]]></code>
<code><![CDATA[$option->children()]]></code>
</PossiblyNullArgument>
<PossiblyNullIterator>
<code><![CDATA[$cascadeElement->children()]]></code>
</PossiblyNullIterator>
<TypeDoesNotContainType>
<code><![CDATA[$xmlRoot->getName() === 'embeddable']]></code>
<code><![CDATA[$xmlRoot->getName() === 'entity']]></code>
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Issue9300;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToMany;
/**
* @Entity
*/
class Issue9300Child
{
/**
* @var int
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
public $id;
/**
* @var Collection<int, Issue9300Parent>
* @ManyToMany(targetEntity="Issue9300Parent")
*/
public $parents;
/**
* @var string
* @Column(type="string")
*/
public $name;
public function __construct()
{
$this->parents = new ArrayCollection();
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Issue9300;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
/**
* @Entity
*/
class Issue9300Parent
{
/**
* @var int
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
public $id;
/**
* @var string
* @Column(type="string")
*/
public $name;
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\Exception\EntityIdentityCollisionException;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\PersistentCollection;
@@ -20,7 +21,6 @@ use Doctrine\Tests\Models\CMS\CmsPhonenumber;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\OrmFunctionalTestCase;
use InvalidArgumentException;
use RuntimeException;
use function get_class;
@@ -1329,6 +1329,8 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
public function testItThrowsWhenReferenceUsesIdAssignedByDatabase(): void
{
$this->_em->getConfiguration()->setRejectIdCollisionInIdentityMap(true);
$user = new CmsUser();
$user->name = 'test';
$user->username = 'test';
@@ -1345,7 +1347,7 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
// Now the database will assign an ID to the $user2 entity, but that place
// in the identity map is already taken by user error.
$this->expectException(RuntimeException::class);
$this->expectException(EntityIdentityCollisionException::class);
$this->expectExceptionMessageMatches('/another object .* was already present for the same ID/');
// depending on ID generation strategy, the ID may be asssigned already here
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use function reset;
class GH10880Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH10880BaseProcess::class,
GH10880Process::class,
GH10880ProcessOwner::class,
]);
}
public function testProcessShouldBeUpdated(): void
{
$process = new GH10880Process();
$process->description = 'first value';
$owner = new GH10880ProcessOwner();
$owner->process = $process;
$this->_em->persist($process);
$this->_em->persist($owner);
$this->_em->flush();
$this->_em->clear();
$ownerLoaded = $this->_em->getRepository(GH10880ProcessOwner::class)->find($owner->id);
$ownerLoaded->process->description = 'other description';
$queryLog = $this->getQueryLog();
$queryLog->reset()->enable();
$this->_em->flush();
$this->removeTransactionCommandsFromQueryLog();
self::assertCount(1, $queryLog->queries);
$query = reset($queryLog->queries);
self::assertSame('UPDATE GH10880BaseProcess SET description = ? WHERE id = ?', $query['sql']);
}
private function removeTransactionCommandsFromQueryLog(): void
{
$log = $this->getQueryLog();
foreach ($log->queries as $key => $entry) {
if ($entry['sql'] === '"START TRANSACTION"' || $entry['sql'] === '"COMMIT"') {
unset($log->queries[$key]);
}
}
}
}
/**
* @ORM\Entity
*/
class GH10880ProcessOwner
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* fetch=EAGER is important to reach the part of \Doctrine\ORM\UnitOfWork::createEntity()
* that is important for this regression test
*
* @ORM\ManyToOne(targetEntity="GH10880Process", fetch="EAGER")
*
* @var GH10880Process
*/
public $process;
}
/**
* @ORM\Entity()
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({"process" = "GH10880Process"})
*/
abstract class GH10880BaseProcess
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\Column(type="text")
*
* @var string
*/
public $description;
}
/**
* @ORM\Entity
*/
class GH10880Process extends GH10880BaseProcess
{
}
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Tests\Models\Issue9300\Issue9300Child;
use Doctrine\Tests\Models\Issue9300\Issue9300Parent;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @group GH-9300
*/
class Issue9300Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
$this->useModelSet('issue9300');
parent::setUp();
}
/**
* @group GH-9300
*/
public function testPersistedCollectionIsPresentInOriginalDataAfterFlush(): void
{
$parent = new Issue9300Parent();
$child = new Issue9300Child();
$child->parents->add($parent);
$parent->name = 'abc';
$child->name = 'abc';
$this->_em->persist($parent);
$this->_em->persist($child);
$this->_em->flush();
$parent->name = 'abcd';
$child->name = 'abcd';
$this->_em->flush();
self::assertArrayHasKey('parents', $this->_em->getUnitOfWork()->getOriginalEntityData($child));
}
/**
* @group GH-9300
*/
public function testPersistingCollectionAfterFlushWorksAsExpected(): void
{
$parentOne = new Issue9300Parent();
$parentTwo = new Issue9300Parent();
$childOne = new Issue9300Child();
$parentOne->name = 'abc';
$parentTwo->name = 'abc';
$childOne->name = 'abc';
$childOne->parents = new ArrayCollection([$parentOne]);
$this->_em->persist($parentOne);
$this->_em->persist($parentTwo);
$this->_em->persist($childOne);
$this->_em->flush();
// Recalculate change-set -> new original data
$childOne->name = 'abcd';
$this->_em->flush();
$childOne->parents = new ArrayCollection([$parentTwo]);
$this->_em->flush();
$this->_em->clear();
$childOneFresh = $this->_em->find(Issue9300Child::class, $childOne->id);
self::assertCount(1, $childOneFresh->parents);
self::assertEquals($parentTwo->id, $childOneFresh->parents[0]->id);
}
}
+23 -3
View File
@@ -13,6 +13,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\Events;
use Doctrine\ORM\Exception\EntityIdentityCollisionException;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
@@ -41,7 +42,6 @@ use Doctrine\Tests\Models\GeoNames\Country;
use Doctrine\Tests\OrmTestCase;
use Doctrine\Tests\PHPUnitCompatibility\MockBuilderCompatibilityTools;
use PHPUnit\Framework\MockObject\MockObject;
use RuntimeException;
use stdClass;
use function assert;
@@ -927,7 +927,7 @@ class UnitOfWorkTest extends OrmTestCase
self::assertEmpty($user->phonenumbers->getSnapshot());
}
public function testItThrowsWhenApplicationProvidedIdsCollide(): void
public function testItTriggersADeprecationNoticeWhenApplicationProvidedIdsCollide(): void
{
// We're using application-provided IDs and assign the same ID twice
// Note this is about colliding IDs in the identity map in memory.
@@ -940,7 +940,27 @@ class UnitOfWorkTest extends OrmTestCase
$phone2 = new CmsPhonenumber();
$phone2->phonenumber = '1234';
$this->expectException(RuntimeException::class);
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/10785');
$this->_unitOfWork->persist($phone2);
}
public function testItThrowsWhenApplicationProvidedIdsCollide(): void
{
$this->_emMock->getConfiguration()->setRejectIdCollisionInIdentityMap(true);
// We're using application-provided IDs and assign the same ID twice
// Note this is about colliding IDs in the identity map in memory.
// Duplicate database-level IDs would be spotted when the EM is flushed.
$phone1 = new CmsPhonenumber();
$phone1->phonenumber = '1234';
$this->_unitOfWork->persist($phone1);
$phone2 = new CmsPhonenumber();
$phone2->phonenumber = '1234';
$this->expectException(EntityIdentityCollisionException::class);
$this->expectExceptionMessageMatches('/another object .* was already present for the same ID/');
$this->_unitOfWork->persist($phone2);
@@ -338,6 +338,10 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
Models\Issue5989\Issue5989Employee::class,
Models\Issue5989\Issue5989Manager::class,
],
'issue9300' => [
Models\Issue9300\Issue9300Child::class,
Models\Issue9300\Issue9300Parent::class,
],
];
/** @param class-string ...$models */