Skip to content

Commit 0ee9fb8

Browse files
committed
fix: only call om::persist() once all objects are created
1 parent 6097b89 commit 0ee9fb8

File tree

5 files changed

+158
-41
lines changed

5 files changed

+158
-41
lines changed

src/Persistence/PersistenceManager.php

+48-23
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,28 @@ 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+
$this->callPostPersistCallbacks();
104+
}
105+
81106
/**
82107
* @template T of object
83108
*
@@ -92,25 +117,13 @@ public function scheduleForInsert(object $object, array $afterPersistCallbacks =
92117
$object = unproxy($object);
93118
}
94119

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

98122
$this->afterPersistCallbacks = [...$this->afterPersistCallbacks, ...$afterPersistCallbacks];
99123

100124
return $object;
101125
}
102126

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-
114127
/**
115128
* @template T
116129
*
@@ -126,31 +139,43 @@ public function flushAfter(callable $callback): mixed
126139

127140
$this->flush = true;
128141

142+
$this->flushAllStrategies();
143+
144+
$this->callPostPersistCallbacks();
145+
146+
return $result;
147+
}
148+
149+
private function flushAllStrategies(): void
150+
{
129151
foreach ($this->strategies as $strategy) {
130152
foreach ($strategy->objectManagers() as $om) {
131153
$this->flush($om);
132154
}
133155
}
134-
135-
return $result;
136156
}
137157

138158
public function flush(ObjectManager $om): void
139159
{
140160
if ($this->flush) {
141161
$om->flush();
162+
}
163+
}
142164

143-
if ($this->afterPersistCallbacks) {
144-
$afterPersistCallbacks = $this->afterPersistCallbacks;
145-
$this->afterPersistCallbacks = [];
165+
private function callPostPersistCallbacks(): void
166+
{
167+
if (!$this->flush || $this->afterPersistCallbacks === []) {
168+
return;
169+
}
146170

147-
foreach ($afterPersistCallbacks as $afterPersistCallback) {
148-
$afterPersistCallback();
149-
}
171+
$afterPersistCallbacks = $this->afterPersistCallbacks;
172+
$this->afterPersistCallbacks = [];
150173

151-
$this->flush($om);
152-
}
174+
foreach ($afterPersistCallbacks as $afterPersistCallback) {
175+
$afterPersistCallback();
153176
}
177+
178+
$this->flushAllStrategies();
154179
}
155180

156181
/**

src/Persistence/PersistentObjectFactory.php

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

221-
$configuration->persistence()->save($object);
221+
$configuration->persistence()->saveAll();
222222

223223
return $object;
224224
}
@@ -298,25 +298,23 @@ protected function normalizeParameter(string $field, mixed $value): mixed
298298
if ($inversedRelationshipMetadata && !$inversedRelationshipMetadata->isCollection) {
299299
$inverseField = $inversedRelationshipMetadata->inverseField;
300300

301-
$inversedObject = $value->withPersistMode(
302-
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
303-
)
304-
->notRootFactory()
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+
$inverseObjectPlaceholder = (new \ReflectionClass($value::class()))->newInstanceWithoutConstructor();
305304

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

312-
$inversedObject = unproxy($inversedObject, withAutoRefresh: false);
312+
$inverseObject = unproxy($inverseObject, withAutoRefresh: false);
313313

314-
$this->tempAfterInstantiate[] = static function(object $object) use ($inversedObject, $inverseField, $pm, $placeholder) {
315-
$pm->forget($placeholder);
316-
set($inversedObject, $inverseField, $object);
314+
set($object, $field, $inverseObject);
317315
};
318316

319-
return $inversedObject;
317+
return $inverseObjectPlaceholder;
320318
} else {
321319
$value = $value->notRootFactory();
322320
}
@@ -389,6 +387,8 @@ protected function normalizeObject(object $object): object
389387

390388
if (!$persistenceManager->isPersisted($object)) {
391389
$persistenceManager->scheduleForInsert($object);
390+
391+
return $object;
392392
}
393393

394394
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

+22-3
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;
@@ -100,7 +101,7 @@ public function inverse_one_to_one_with_both_nullable(): void
100101
#[DataProvider('provideCascadeRelationshipsCombinations')]
101102
#[UsingRelationships(InversedOneToOneWithOneToMany\OwningSide::class, ['inverseSide'])]
102103
#[UsingRelationships(InversedOneToOneWithOneToMany\Item::class, ['owningSide'])]
103-
#[RequiresPhpunit('^11.4')]
104+
#[RequiresPhpunit('>=11.4')]
104105
public function inverse_one_to_one_with_one_to_many(): void
105106
{
106107
$inverseSideFactory = persistent_factory(InversedOneToOneWithOneToMany\InverseSide::class);
@@ -141,7 +142,7 @@ public function many_to_many_to_self_referencing_inverse_side(): void
141142
#[Test]
142143
#[DataProvider('provideCascadeRelationshipsCombinations')]
143144
#[UsingRelationships(IndexedOneToMany\ParentEntity::class, ['items'])]
144-
#[RequiresPhpunit('^11.4')]
145+
#[RequiresPhpunit('>=11.4')]
145146
public function indexed_one_to_many(): void
146147
{
147148
$parentFactory = persistent_factory(IndexedOneToMany\ParentEntity::class);
@@ -163,7 +164,7 @@ public function indexed_one_to_many(): void
163164
#[Test]
164165
#[DataProvider('provideCascadeRelationshipsCombinations')]
165166
#[UsingRelationships(InversedOneToOneWithManyToOne\InverseSide::class, ['owningSide', 'item'])]
166-
#[RequiresPhpunit('^11.4')]
167+
#[RequiresPhpunit('>=11.4')]
167168
public function inversed_one_to_one_can_be_used_after_other_relationship(): void
168169
{
169170
$inverseSideFactory = persistent_factory(InversedOneToOneWithManyToOne\InverseSide::class);
@@ -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)