Skip to content

Commit b892369

Browse files
committed
fix: only call om::persist() once all objects are created
1 parent f113b91 commit b892369

File tree

5 files changed

+131
-28
lines changed

5 files changed

+131
-28
lines changed

src/Persistence/PersistenceManager.php

+24-13
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ final class PersistenceManager
3131
private bool $flush = true;
3232
private bool $persist = true;
3333

34+
/** @var list<object> */
35+
private array $objectsToPersist = [];
36+
3437
/** @var list<callable():void> */
3538
private array $afterPersistCallbacks = [];
3639

@@ -78,6 +81,26 @@ public function save(object $object): object
7881
return $object;
7982
}
8083

84+
public function saveAll(): void
85+
{
86+
$objectManagers = [];
87+
88+
foreach ($this->objectsToPersist as $object) {
89+
$om = $this->strategyFor($object::class)->objectManagerFor($object::class);
90+
$om->persist($object);
91+
92+
if (!in_array($om, $objectManagers, true)) {
93+
$objectManagers[] = $om;
94+
}
95+
}
96+
97+
$this->objectsToPersist = [];
98+
99+
foreach ($objectManagers as $om) {
100+
$this->flush($om);
101+
}
102+
}
103+
81104
/**
82105
* @template T of object
83106
*
@@ -92,25 +115,13 @@ public function scheduleForInsert(object $object, array $afterPersistCallbacks =
92115
$object = unproxy($object);
93116
}
94117

95-
$om = $this->strategyFor($object::class)->objectManagerFor($object::class);
96-
$om->persist($object);
118+
$this->objectsToPersist[] = $object;
97119

98120
$this->afterPersistCallbacks = [...$this->afterPersistCallbacks, ...$afterPersistCallbacks];
99121

100122
return $object;
101123
}
102124

103-
public function forget(object $object): void
104-
{
105-
if ($this->isPersisted($object)) {
106-
throw new \LogicException('Cannot forget an object already persisted.');
107-
}
108-
109-
$om = $this->strategyFor($object::class)->objectManagerFor($object::class);
110-
111-
$om->detach($object);
112-
}
113-
114125
/**
115126
* @template T
116127
*

src/Persistence/PersistentObjectFactory.php

+15-15
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ public function create(callable|array $attributes = []): object
217217
throw new \LogicException('Persistence cannot be used in unit tests.');
218218
}
219219

220-
$configuration->persistence()->save($object);
220+
$configuration->persistence()->saveAll();
221221

222222
return $object;
223223
}
@@ -293,25 +293,23 @@ protected function normalizeParameter(string $field, mixed $value): mixed
293293
if ($inversedRelationshipMetadata && !$inversedRelationshipMetadata->isCollection) {
294294
$inverseField = $inversedRelationshipMetadata->inverseField;
295295

296-
$inversedObject = $value->withPersistMode(
297-
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
298-
)
299-
->notRootFactory()
296+
// we need to handle the circular dependency involved by inversed one-to-one relationship:
297+
// a placeholder object is used, which will be replaced by the real object, after its instantiation
298+
$inverseObjectPlaceholder = (new \ReflectionClass($value::class()))->newInstanceWithoutConstructor();
300299

301-
// we need to handle the circular dependency involved by inversed one-to-one relationship:
302-
// a placeholder object is used, which will be replaced by the real object, after its instantiation
303-
->create([
304-
$inverseField => $placeholder = (new \ReflectionClass(static::class()))->newInstanceWithoutConstructor(),
305-
]);
300+
$this->tempAfterInstantiate[] = function(object $object) use ($value, $inverseField, $field) {
301+
$inverseObject = $value->withPersistMode(
302+
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
303+
)
304+
->notRootFactory()
305+
->create([$inverseField => $object]);
306306

307-
$inversedObject = unproxy($inversedObject, withAutoRefresh: false);
307+
$inverseObject = unproxy($inverseObject, withAutoRefresh: false);
308308

309-
$this->tempAfterInstantiate[] = static function(object $object) use ($inversedObject, $inverseField, $pm, $placeholder) {
310-
$pm->forget($placeholder);
311-
set($inversedObject, $inverseField, $object);
309+
set($object, $field, $inverseObject);
312310
};
313311

314-
return $inversedObject;
312+
return $inverseObjectPlaceholder;
315313
} else {
316314
$value = $value->notRootFactory();
317315
}
@@ -384,6 +382,8 @@ protected function normalizeObject(object $object): object
384382

385383
if (!$persistenceManager->isPersisted($object)) {
386384
$persistenceManager->scheduleForInsert($object);
385+
386+
return $object;
387387
}
388388

389389
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithoutAutoGeneratedId;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
use Symfony\Component\Uid\Uuid;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <[email protected]>
21+
*/
22+
#[ORM\Entity]
23+
#[ORM\Table('inversed_one_to_one_with_custom_id_inverse')]
24+
class InverseSide
25+
{
26+
#[ORM\Id]
27+
#[ORM\Column(type: 'uuid')]
28+
public Uuid $id;
29+
30+
public function __construct(
31+
#[ORM\OneToOne(mappedBy: 'inverseSide', cascade: ['persist'])] // @phpstan-ignore doctrine.associationType
32+
public OwningSide $owningSide,
33+
) {
34+
$this->id = Uuid::v7();
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithoutAutoGeneratedId;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
use Symfony\Component\Uid\Uuid;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <[email protected]>
21+
*/
22+
#[ORM\Entity]
23+
#[ORM\Table('inversed_one_to_one_with_custom_id_owning')]
24+
class OwningSide
25+
{
26+
#[ORM\Id]
27+
#[ORM\Column(type: 'uuid')]
28+
public Uuid $id;
29+
30+
#[ORM\OneToOne(inversedBy: 'owningSide', cascade: ['persist'])]
31+
public ?InverseSide $inverseSide = null;
32+
33+
public function __construct()
34+
{
35+
$this->id = Uuid::v7();
36+
}
37+
}

tests/Integration/ORM/EdgeCasesRelationshipTest.php

+19
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\IndexedOneToMany;
2525
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithManyToOne;
2626
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning;
27+
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithoutAutoGeneratedId;
2728
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany;
2829
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithSetter;
2930
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing;
@@ -187,6 +188,24 @@ public function inversed_one_to_one_can_be_used_after_other_relationship(): void
187188
self::assertNotNull($inverseSide->item);
188189
}
189190

191+
/** @test */
192+
#[Test]
193+
#[DataProvider('provideCascadeRelationshipsCombinations')]
194+
#[UsingRelationships(InversedOneToOneWithoutAutoGeneratedId\OwningSide::class, ['inverseSide'])]
195+
#[RequiresPhpunit('>=11.4')]
196+
public function inverse_one_to_one_with_custom_id(): void
197+
{
198+
$owningSideFactory = persistent_factory(InversedOneToOneWithoutAutoGeneratedId\OwningSide::class);
199+
$inverseSideFactory = persistent_factory(InversedOneToOneWithoutAutoGeneratedId\InverseSide::class);
200+
201+
$inverseSide = $inverseSideFactory->create(['owningSide' => $owningSideFactory]);
202+
203+
$owningSideFactory::assert()->count(1);
204+
$inverseSideFactory::assert()->count(1);
205+
206+
self::assertSame($inverseSide, $inverseSide->owningSide->inverseSide);
207+
}
208+
190209
/**
191210
* @test
192211
*/

0 commit comments

Comments
 (0)