Skip to content

Commit 9fb9cc4

Browse files
committed
Merge remote-tracking branch 'origin/2.20.x' into 3.5.x
2 parents 781ed30 + c322c71 commit 9fb9cc4

File tree

5 files changed

+284
-28
lines changed

5 files changed

+284
-28
lines changed

src/Persisters/Entity/BasicEntityPersister.php

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,6 @@ class BasicEntityPersister implements EntityPersister
153153
*/
154154
protected array $quotedColumns = [];
155155

156-
/**
157-
* The INSERT SQL statement used for entities handled by this persister.
158-
* This SQL is only generated once per request, if at all.
159-
*/
160-
private string|null $insertSql = null;
161-
162156
/**
163157
* The quote strategy.
164158
*/
@@ -273,8 +267,8 @@ public function executeInserts(): void
273267
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
274268
}
275269

276-
// Unset this queued insert, so that the prepareUpdateData() method knows right away
277-
// (for the next entity already) that the current entity has been written to the database
270+
// Unset this queued insert, so that the prepareUpdateData() method (called via prepareInsertData() method)
271+
// knows right away (for the next entity already) that the current entity has been written to the database
278272
// and no extra updates need to be scheduled to refer to it.
279273
//
280274
// In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
@@ -1418,22 +1412,17 @@ protected function getSelectManyToManyJoinSQL(AssociationMapping&ManyToManyAssoc
14181412

14191413
public function getInsertSQL(): string
14201414
{
1421-
if ($this->insertSql !== null) {
1422-
return $this->insertSql;
1423-
}
1424-
14251415
$columns = $this->getInsertColumnList();
14261416
$tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
14271417

1428-
if (empty($columns)) {
1429-
$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
1430-
$this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
1418+
if ($columns === []) {
1419+
$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
14311420

1432-
return $this->insertSql;
1421+
return $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
14331422
}
14341423

1435-
$values = [];
1436-
$columns = array_unique($columns);
1424+
$placeholders = [];
1425+
$columns = array_unique($columns);
14371426

14381427
foreach ($columns as $column) {
14391428
$placeholder = '?';
@@ -1447,15 +1436,13 @@ public function getInsertSQL(): string
14471436
$placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
14481437
}
14491438

1450-
$values[] = $placeholder;
1439+
$placeholders[] = $placeholder;
14511440
}
14521441

1453-
$columns = implode(', ', $columns);
1454-
$values = implode(', ', $values);
1455-
1456-
$this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);
1442+
$columns = implode(', ', $columns);
1443+
$placeholders = implode(', ', $placeholders);
14571444

1458-
return $this->insertSql;
1445+
return sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $placeholders);
14591446
}
14601447

14611448
/**

src/Persisters/Entity/JoinedSubclassPersister.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public function executeInserts(): void
134134
// Execute all inserts. For each entity:
135135
// 1) Insert on root table
136136
// 2) Insert on sub tables
137-
foreach ($this->queuedInserts as $entity) {
137+
foreach ($this->queuedInserts as $key => $entity) {
138138
$insertData = $this->prepareInsertData($entity);
139139

140140
// Execute insert on root table
@@ -179,9 +179,16 @@ public function executeInserts(): void
179179
if ($this->class->requiresFetchAfterChange) {
180180
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
181181
}
182-
}
183182

184-
$this->queuedInserts = [];
183+
// Unset this queued insert, so that the prepareUpdateData() method (called via prepareInsertData() method)
184+
// knows right away (for the next entity already) that the current entity has been written to the database
185+
// and no extra updates need to be scheduled to refer to it.
186+
//
187+
// In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
188+
// from its own list (\Doctrine\ORM\UnitOfWork::$entityInsertions) right after they
189+
// were given to our addInsert() method.
190+
unset($this->queuedInserts[$key]);
191+
}
185192
}
186193

187194
public function update(object $entity): void

src/UnitOfWork.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -934,7 +934,9 @@ private function persistNew(ClassMetadata $class, object $entity): void
934934

935935
$this->entityStates[$oid] = self::STATE_MANAGED;
936936

937-
$this->scheduleForInsert($entity);
937+
if (! isset($this->entityInsertions[$oid])) {
938+
$this->scheduleForInsert($entity);
939+
}
938940
}
939941

940942
/** @param mixed[] $idValue */
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional;
6+
7+
use Doctrine\ORM\Event\PrePersistEventArgs;
8+
use Doctrine\ORM\Events;
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\ManyToOne;
14+
use Doctrine\Tests\OrmFunctionalTestCase;
15+
16+
use function uniqid;
17+
18+
class PrePersistEventTest extends OrmFunctionalTestCase
19+
{
20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
$this->createSchemaForModels(
25+
EntityWithUnmappedEntity::class,
26+
EntityWithCascadeAssociation::class,
27+
);
28+
}
29+
30+
public function testCallingPersistInPrePersistHook(): void
31+
{
32+
$entityWithUnmapped = new EntityWithUnmappedEntity();
33+
$entityWithCascade = new EntityWithCascadeAssociation();
34+
35+
$entityWithUnmapped->unmapped = $entityWithCascade;
36+
$entityWithCascade->cascaded = $entityWithUnmapped;
37+
38+
$this->_em->getEventManager()->addEventListener(Events::prePersist, new PrePersistUnmappedPersistListener());
39+
$this->_em->persist($entityWithUnmapped);
40+
41+
$this->assertTrue($this->_em->getUnitOfWork()->isScheduledForInsert($entityWithCascade));
42+
$this->assertTrue($this->_em->getUnitOfWork()->isScheduledForInsert($entityWithUnmapped));
43+
}
44+
}
45+
46+
class PrePersistUnmappedPersistListener
47+
{
48+
public function prePersist(PrePersistEventArgs $args): void
49+
{
50+
$object = $args->getObject();
51+
52+
if ($object instanceof EntityWithUnmappedEntity) {
53+
$uow = $args->getObjectManager()->getUnitOfWork();
54+
55+
if ($object->unmapped && ! $uow->isInIdentityMap($object->unmapped) && ! $uow->isScheduledForInsert($object->unmapped)) {
56+
$args->getObjectManager()->persist($object->unmapped);
57+
}
58+
}
59+
}
60+
}
61+
62+
#[Entity]
63+
class EntityWithUnmappedEntity
64+
{
65+
#[Id]
66+
#[Column(type: 'string', length: 255)]
67+
#[GeneratedValue(strategy: 'NONE')]
68+
public string $id;
69+
70+
public EntityWithCascadeAssociation|null $unmapped = null;
71+
72+
public function __construct()
73+
{
74+
$this->id = uniqid(self::class, true);
75+
}
76+
}
77+
78+
#[Entity]
79+
class EntityWithCascadeAssociation
80+
{
81+
#[Id]
82+
#[Column(type: 'string', length: 255)]
83+
#[GeneratedValue(strategy: 'NONE')]
84+
public string $id;
85+
86+
#[ManyToOne(targetEntity: EntityWithUnmappedEntity::class, cascade: ['persist'])]
87+
public EntityWithUnmappedEntity|null $cascaded = null;
88+
89+
public function __construct()
90+
{
91+
$this->id = uniqid(self::class, true);
92+
}
93+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional;
6+
7+
use Doctrine\ORM\Id\AssignedGenerator;
8+
use Doctrine\ORM\Mapping\ClassMetadata;
9+
use Doctrine\Tests\Models\Cache\Country;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use PHPUnit\Framework\Attributes\Group;
12+
use ReflectionProperty;
13+
14+
use function array_diff;
15+
use function array_filter;
16+
use function file_exists;
17+
use function rmdir;
18+
use function scandir;
19+
use function strpos;
20+
use function sys_get_temp_dir;
21+
22+
use const DIRECTORY_SEPARATOR;
23+
24+
/** @phpstan-type SupportedCacheUsage 0|ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE|ClassMetadata::CACHE_USAGE_READ_WRITE */
25+
#[Group('DDC-2183')]
26+
class SecondLevelCacheCountQueriesTest extends SecondLevelCacheFunctionalTestCase
27+
{
28+
/** @var string */
29+
private $tmpDir;
30+
31+
protected function tearDown(): void
32+
{
33+
if ($this->tmpDir !== null && file_exists($this->tmpDir)) {
34+
foreach (array_diff(scandir($this->tmpDir), ['.', '..']) as $f) {
35+
rmdir($this->tmpDir . DIRECTORY_SEPARATOR . $f);
36+
}
37+
38+
rmdir($this->tmpDir);
39+
}
40+
41+
parent::tearDown();
42+
}
43+
44+
/** @param SupportedCacheUsage $cacheUsage */
45+
private function setupCountryModel(int $cacheUsage): void
46+
{
47+
$metadata = $this->_em->getClassMetaData(Country::class);
48+
49+
if ($cacheUsage === 0) {
50+
$metadataCacheReflection = new ReflectionProperty(ClassMetadata::class, 'cache');
51+
$metadataCacheReflection->setAccessible(true);
52+
$metadataCacheReflection->setValue($metadata, null);
53+
54+
return;
55+
}
56+
57+
if ($cacheUsage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
58+
$this->tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::class;
59+
$this->secondLevelCacheFactory->setFileLockRegionDirectory($this->tmpDir);
60+
}
61+
62+
$metadata->enableCache(['usage' => $cacheUsage]);
63+
}
64+
65+
private function loadFixturesCountriesWithoutPostInsertIdentifier(): void
66+
{
67+
$metadata = $this->_em->getClassMetaData(Country::class);
68+
$metadata->setIdGenerator(new AssignedGenerator());
69+
70+
$c1 = new Country('Brazil');
71+
$c1->setId(10);
72+
$c2 = new Country('Germany');
73+
$c2->setId(20);
74+
75+
$this->countries[] = $c1;
76+
$this->countries[] = $c2;
77+
78+
$this->_em->persist($c1);
79+
$this->_em->persist($c2);
80+
$this->_em->flush();
81+
}
82+
83+
/** @param 'INSERT'|'UPDATE'|'DELETE' $type */
84+
private function assertQueryCountByType(string $type, int $expectedCount): void
85+
{
86+
$queries = array_filter($this->getQueryLog()->queries, static function (array $entry) use ($type): bool {
87+
return strpos($entry['sql'], $type) === 0;
88+
});
89+
90+
self::assertCount($expectedCount, $queries);
91+
}
92+
93+
/** @param SupportedCacheUsage $cacheUsage */
94+
#[DataProvider('cacheUsageProvider')]
95+
public function testInsertWithPostInsertIdentifier(int $cacheUsage): void
96+
{
97+
$this->setupCountryModel($cacheUsage);
98+
99+
self::assertQueryCountByType('INSERT', 0);
100+
101+
$this->loadFixturesCountries();
102+
103+
self::assertCount(2, $this->countries);
104+
self::assertQueryCountByType('INSERT', 2);
105+
}
106+
107+
/** @param SupportedCacheUsage $cacheUsage */
108+
#[DataProvider('cacheUsageProvider')]
109+
public function testInsertWithoutPostInsertIdentifier(int $cacheUsage): void
110+
{
111+
$this->setupCountryModel($cacheUsage);
112+
113+
self::assertQueryCountByType('INSERT', 0);
114+
115+
$this->loadFixturesCountriesWithoutPostInsertIdentifier();
116+
117+
self::assertCount(2, $this->countries);
118+
self::assertQueryCountByType('INSERT', 2);
119+
}
120+
121+
/** @param SupportedCacheUsage $cacheUsage */
122+
#[DataProvider('cacheUsageProvider')]
123+
public function testDelete(int $cacheUsage): void
124+
{
125+
$this->setupCountryModel($cacheUsage);
126+
$this->loadFixturesCountries();
127+
128+
$c1 = $this->_em->find(Country::class, $this->countries[0]->getId());
129+
$c2 = $this->_em->find(Country::class, $this->countries[1]->getId());
130+
131+
$this->_em->remove($c1);
132+
$this->_em->remove($c2);
133+
$this->_em->flush();
134+
135+
self::assertQueryCountByType('DELETE', 2);
136+
}
137+
138+
/** @param SupportedCacheUsage $cacheUsage */
139+
#[DataProvider('cacheUsageProvider')]
140+
public function testUpdate(int $cacheUsage): void
141+
{
142+
$this->setupCountryModel($cacheUsage);
143+
$this->loadFixturesCountries();
144+
145+
$c1 = $this->_em->find(Country::class, $this->countries[0]->getId());
146+
$c2 = $this->_em->find(Country::class, $this->countries[1]->getId());
147+
148+
$c1->setName('Czech Republic');
149+
$c2->setName('Hungary');
150+
151+
$this->_em->persist($c1);
152+
$this->_em->persist($c2);
153+
$this->_em->flush();
154+
155+
self::assertQueryCountByType('UPDATE', 2);
156+
}
157+
158+
/** @return list<array{SupportedCacheUsage}> */
159+
public static function cacheUsageProvider(): array
160+
{
161+
return [
162+
[0],
163+
[ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE],
164+
[ClassMetadata::CACHE_USAGE_READ_WRITE],
165+
];
166+
}
167+
}

0 commit comments

Comments
 (0)