Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f113b91

Browse files
committedMar 7, 2025
fix: flush only once per Factory::create() calls in userland
1 parent 2f523df commit f113b91

File tree

8 files changed

+199
-12
lines changed

8 files changed

+199
-12
lines changed
 

‎src/Persistence/PersistenceManager.php

+11-11
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,6 @@ public function save(object $object): object
7575
$om->persist($object);
7676
$this->flush($om);
7777

78-
if ($this->afterPersistCallbacks) {
79-
$afterPersistCallbacks = $this->afterPersistCallbacks;
80-
$this->afterPersistCallbacks = [];
81-
82-
foreach ($afterPersistCallbacks as $afterPersistCallback) {
83-
$afterPersistCallback();
84-
}
85-
86-
$this->save($object);
87-
}
88-
8978
return $object;
9079
}
9180

@@ -150,6 +139,17 @@ public function flush(ObjectManager $om): void
150139
{
151140
if ($this->flush) {
152141
$om->flush();
142+
143+
if ($this->afterPersistCallbacks) {
144+
$afterPersistCallbacks = $this->afterPersistCallbacks;
145+
$this->afterPersistCallbacks = [];
146+
147+
foreach ($afterPersistCallbacks as $afterPersistCallback) {
148+
$afterPersistCallback();
149+
}
150+
151+
$this->flush($om);
152+
}
153153
}
154154
}
155155

‎src/Persistence/PersistentObjectFactory.php

+14-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ abstract class PersistentObjectFactory extends ObjectFactory
4343
/** @var list<callable(T):void> */
4444
private array $tempAfterInstantiate = [];
4545

46+
private bool $isRootFactory = true;
47+
4648
/**
4749
* @phpstan-param mixed|Parameters $criteriaOrId
4850
*
@@ -205,7 +207,7 @@ public function create(callable|array $attributes = []): object
205207

206208
$this->throwIfCannotCreateObject();
207209

208-
if (PersistMode::PERSIST !== $this->persistMode()) {
210+
if (PersistMode::PERSIST !== $this->persistMode() || !$this->isRootFactory) {
209211
return $object;
210212
}
211213

@@ -294,6 +296,7 @@ protected function normalizeParameter(string $field, mixed $value): mixed
294296
$inversedObject = $value->withPersistMode(
295297
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
296298
)
299+
->notRootFactory()
297300

298301
// we need to handle the circular dependency involved by inversed one-to-one relationship:
299302
// a placeholder object is used, which will be replaced by the real object, after its instantiation
@@ -309,6 +312,8 @@ protected function normalizeParameter(string $field, mixed $value): mixed
309312
};
310313

311314
return $inversedObject;
315+
} else {
316+
$value = $value->notRootFactory();
312317
}
313318
}
314319

@@ -445,4 +450,12 @@ private function throwIfCannotCreateObject(): void
445450

446451
throw new \LogicException(\sprintf('Cannot create object in a data provider for non-proxy factories. Transform your factory into a "%s", or call "create()" method in the test. See https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-data-providers', PersistentProxyObjectFactory::class));
447452
}
453+
454+
private function notRootFactory(): static
455+
{
456+
$clone = clone $this;
457+
$clone->isRootFactory = false;
458+
459+
return $clone;
460+
}
448461
}
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 <kevinbond@gmail.com>
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\InversedOneToOneWithManyToOne;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
use Zenstruck\Foundry\Tests\Fixture\Model\Base;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <nikophil@gmail.com>
21+
*/
22+
#[ORM\Entity]
23+
#[ORM\Table('inversed_one_to_one_with_many_to_one_inverse_side')]
24+
class InverseSide extends Base
25+
{
26+
#[ORM\OneToOne(mappedBy: 'inverseSide')]
27+
public ?OwningSide $owningSide = null;
28+
29+
#[ORM\ManyToOne()]
30+
public ?Item $item = null;
31+
32+
public function __construct(
33+
#[ORM\Column()]
34+
public string $mandatoryField
35+
) {
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 <kevinbond@gmail.com>
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\InversedOneToOneWithManyToOne;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
use Zenstruck\Foundry\Tests\Fixture\Model\Base;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <nikophil@gmail.com>
21+
*/
22+
#[ORM\Entity]
23+
#[ORM\Table('inversed_one_to_one_with_many_to_one_item_if_collection')]
24+
class Item extends Base
25+
{
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 <kevinbond@gmail.com>
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\InversedOneToOneWithManyToOne;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
use Zenstruck\Foundry\Tests\Fixture\Model\Base;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <nikophil@gmail.com>
21+
*/
22+
#[ORM\Entity]
23+
#[ORM\Table('inversed_one_to_one_with_many_to_one_owning_side')]
24+
class OwningSide extends Base
25+
{
26+
#[ORM\OneToOne(inversedBy: 'owningSide')]
27+
public ?InverseSide $inverseSide = null;
28+
}

‎tests/Integration/ORM/EdgeCasesRelationshipTest.php

+29
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangesEntityRelationshipCascadePersist;
2323
use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships;
2424
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\IndexedOneToMany;
25+
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithManyToOne;
2526
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning;
2627
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany;
2728
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithSetter;
@@ -158,6 +159,34 @@ public function indexed_one_to_many(): void
158159
self::assertNotNull($parent->getItems()->get('en')); // @phpstan-ignore argument.type
159160
}
160161

162+
/** @test */
163+
#[Test]
164+
#[DataProvider('provideCascadeRelationshipsCombinations')]
165+
#[UsingRelationships(InversedOneToOneWithManyToOne\InverseSide::class, ['owningSide', 'item'])]
166+
#[RequiresPhpunit('^11.4')]
167+
public function inversed_one_to_one_can_be_used_after_other_relationship(): void
168+
{
169+
$inverseSideFactory = persistent_factory(InversedOneToOneWithManyToOne\InverseSide::class);
170+
$owningSideFactory = persistent_factory(InversedOneToOneWithManyToOne\OwningSide::class);
171+
$itemFactory = persistent_factory(InversedOneToOneWithManyToOne\Item::class);
172+
173+
$inverseSide = $inverseSideFactory->create(
174+
[
175+
'mandatoryField' => 'foo',
176+
'owningSide' => $owningSideFactory,
177+
'item' => $itemFactory,
178+
]
179+
);
180+
181+
$inverseSideFactory::assert()->count(1);
182+
$owningSideFactory::assert()->count(1);
183+
$itemFactory::assert()->count(1);
184+
185+
self::assertNotNull($inverseSide->owningSide);
186+
self::assertSame($inverseSide, $inverseSide->owningSide->inverseSide);
187+
self::assertNotNull($inverseSide->item);
188+
}
189+
161190
/**
162191
* @test
163192
*/

‎tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php

+34
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,40 @@ public function can_call_create_in_after_persist_callback(): void
519519
self::assertSame(unproxy($category), $category->getContacts()[0]?->getCategory());
520520
}
521521

522+
/** @test */
523+
#[Test]
524+
public function can_use_nested_after_persist_callback(): void
525+
{
526+
$contact = static::contactFactory()::createOne(
527+
[
528+
'address' => static::addressFactory()
529+
->afterPersist(function(Address $address) {
530+
$address->setCity('city from after persist');
531+
}),
532+
]
533+
);
534+
535+
self::assertSame('city from after persist', $contact->getAddress()->getCity());
536+
}
537+
538+
/** @test */
539+
#[Test]
540+
public function can_call_create_in_nested_after_persist_callback(): void
541+
{
542+
$contact = static::contactFactory()::createOne(
543+
[
544+
'category' => static::categoryFactory()
545+
->afterPersist(function(Category $category) {
546+
$category->addSecondaryContact(
547+
unproxy(static::contactFactory()::createOne())
548+
);
549+
}),
550+
]
551+
);
552+
553+
self::assertCount(1, $contact->getCategory()?->getSecondaryContacts() ?? []);
554+
}
555+
522556
/** @return PersistentObjectFactory<Contact> */
523557
protected static function contactFactoryWithoutCategory(): PersistentObjectFactory
524558
{

‎tests/Integration/Persistence/GenericFactoryTestCase.php

+20
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Zenstruck\Foundry\Tests\Integration\Persistence;
1313

14+
use PHPUnit\Framework\Attributes\Test;
1415
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
1516
use Zenstruck\Foundry\Configuration;
1617
use Zenstruck\Foundry\Exception\PersistenceDisabled;
@@ -564,6 +565,25 @@ public function can_use_after_persist_with_attributes(): void
564565
$this->assertSame(1, $object->getPropInteger());
565566
}
566567

568+
/**
569+
* @test
570+
*/
571+
#[Test]
572+
public function it_actually_calls_post_persist_hook_after_persist_when_in_flush_after(): void
573+
{
574+
$object = flush_after(
575+
function () {
576+
return static::factory()->afterPersist(
577+
static function (GenericModel $o) {
578+
$o->setProp1((string) $o->id);
579+
}
580+
)->create();
581+
}
582+
);
583+
584+
self::assertSame((string) $object->id, $object->getProp1());
585+
}
586+
567587
/**
568588
* @return class-string<GenericModel>
569589
*/

0 commit comments

Comments
 (0)
Please sign in to comment.