Skip to content

Commit 64bc93e

Browse files
authored
Merge 2.3.x in 2.x (#845)
* fix: can call ->create() in after persist callback (#833) * changelog: update [skip ci] * bot: fix cs [skip ci] * chore: upgrade also dama when upgrading phpunit version (#839) * minor: add parameter "withAutoRefresh" to unproxy() function (#840) * fix: use Doctrine metadata event when persist is disabled (#841) * fix: bug with factory collectin of persistent factory in unit test (#842) * bot: fix cs [skip ci] * fix: add missing Test attribute --------- Co-authored-by: nikophil <[email protected]>
1 parent 48d9249 commit 64bc93e

File tree

9 files changed

+110
-34
lines changed

9 files changed

+110
-34
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# CHANGELOG
22

3+
## [v2.3.6](https://github.com/zenstruck/foundry/releases/tag/v2.3.6)
4+
5+
February 25th, 2025 - [v2.3.5...v2.3.6](https://github.com/zenstruck/foundry/compare/v2.3.5...v2.3.6)
6+
7+
* 300645b fix: can call ->create() in after persist callback (#833) by @nikophil
8+
39
## [v2.3.5](https://github.com/zenstruck/foundry/releases/tag/v2.3.5)
410

511
February 24th, 2025 - [v2.3.4...v2.3.5](https://github.com/zenstruck/foundry/compare/v2.3.4...v2.3.5)

phpunit

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ SHOULD_UPDATE_PHPUNIT=$(check_phpunit_version "${PHPUNIT_VERSION}")
3535

3636
if [ "${SHOULD_UPDATE_PHPUNIT}" = "0" ]; then
3737
echo "ℹ️ Upgrading PHPUnit to ${PHPUNIT_VERSION}"
38-
composer update brianium/paratest "phpunit/phpunit:^${PHPUNIT_VERSION}" -W
38+
composer update dama/doctrine-test-bundle brianium/paratest "phpunit/phpunit:^${PHPUNIT_VERSION}" -W
3939
fi
4040
### <<
4141

src/ObjectFactory.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function __construct()
4646
{
4747
parent::__construct();
4848

49-
$this->validationEnabled = Configuration::instance()->validationEnabled;
49+
$this->validationEnabled = Configuration::isBooted() && Configuration::instance()->validationEnabled;
5050
}
5151

5252
/**
@@ -194,7 +194,7 @@ public function getValidationGroups(): string|GroupSequence|array|null
194194
*/
195195
protected function initializeInternal(): static
196196
{
197-
if (!Configuration::instance()->hasEventDispatcher()) {
197+
if (!Configuration::isBooted() || !Configuration::instance()->hasEventDispatcher()) {
198198
return $this;
199199
}
200200

src/Persistence/PersistentObjectFactory.php

+32-26
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Doctrine\Persistence\ObjectRepository;
1515
use Symfony\Component\VarExporter\Exception\LogicException as VarExportLogicException;
1616
use Zenstruck\Foundry\Configuration;
17+
use Zenstruck\Foundry\Exception\FoundryNotBooted;
1718
use Zenstruck\Foundry\Exception\PersistenceDisabled;
1819
use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
1920
use Zenstruck\Foundry\Factory;
@@ -48,7 +49,7 @@ public function __construct()
4849
{
4950
parent::__construct();
5051

51-
$this->persist = Configuration::instance()->isPersistenceEnabled() ? PersistMode::PERSIST : PersistMode::WITHOUT_PERSISTING;
52+
$this->persist = $this->isPersistenceEnabled() ? PersistMode::PERSIST : PersistMode::WITHOUT_PERSISTING;
5253
}
5354

5455
/**
@@ -271,17 +272,11 @@ final public function afterPersist(callable $callback): static
271272
*/
272273
public function persistMode(): PersistMode
273274
{
274-
return Configuration::instance()->isPersistenceEnabled() ? $this->persist : PersistMode::WITHOUT_PERSISTING;
275+
return $this->isPersistenceEnabled() ? $this->persist : PersistMode::WITHOUT_PERSISTING;
275276
}
276277

277278
final public function isPersisting(): bool
278279
{
279-
$config = Configuration::instance();
280-
281-
if (!$config->isPersistenceEnabled()) {
282-
return false;
283-
}
284-
285280
return $this->persistMode()->isPersisting();
286281
}
287282

@@ -304,16 +299,17 @@ protected function normalizeParameter(string $field, mixed $value): mixed
304299
if ($inversedRelationshipMetadata && !$inversedRelationshipMetadata->isCollection) {
305300
$inverseField = $inversedRelationshipMetadata->inverseField;
306301

307-
// we need to handle the circular dependency involved by inversed one-to-one relationship:
308-
// a placeholder object is used, which will be replaced by the real object, after its instantiation
309-
$inversedObject = $value->withPersistMode(PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT)
310-
->create([$inverseField => $placeholder = (new \ReflectionClass(static::class()))->newInstanceWithoutConstructor()]);
302+
$inversedObject = $value->withPersistMode(
303+
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
304+
)
311305

312-
// auto-refresh computes changeset and prevents the placeholder object to be cleanly
313-
// forgotten fom the persistence manager
314-
if ($inversedObject instanceof Proxy) {
315-
$inversedObject = $inversedObject->_real(withAutoRefresh: false);
316-
}
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+
]);
311+
312+
$inversedObject = unproxy($inversedObject, withAutoRefresh: false);
317313

318314
$this->tempAfterInstantiate[] = static function(object $object) use ($inversedObject, $inverseField, $pm, $placeholder) {
319315
$pm->forget($placeholder);
@@ -324,12 +320,12 @@ protected function normalizeParameter(string $field, mixed $value): mixed
324320
}
325321
}
326322

327-
return unproxy(parent::normalizeParameter($field, $value));
323+
return unproxy(parent::normalizeParameter($field, $value), withAutoRefresh: false);
328324
}
329325

330326
protected function normalizeCollection(string $field, FactoryCollection $collection): array
331327
{
332-
if (!$this->isPersisting() || !$collection->factory instanceof self) {
328+
if (!Configuration::instance()->isPersistenceAvailable() || !$collection->factory instanceof self) {
333329
return parent::normalizeCollection($field, $collection);
334330
}
335331

@@ -338,12 +334,15 @@ protected function normalizeCollection(string $field, FactoryCollection $collect
338334
$inverseRelationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $collection->factory::class(), $field);
339335

340336
if ($inverseRelationshipMetadata && $inverseRelationshipMetadata->isCollection) {
341-
$this->tempAfterInstantiate[] = static function(object $object) use ($collection, $inverseRelationshipMetadata, $field) {
337+
$this->tempAfterInstantiate[] = function(object $object) use ($collection, $inverseRelationshipMetadata, $field) {
342338
$inverseField = $inverseRelationshipMetadata->inverseField;
343339

344-
$inverseObjects = $collection->withPersistMode(PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT)->create([$inverseField => $object]);
340+
$inverseObjects = $collection->withPersistMode(
341+
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
342+
)
343+
->create([$inverseField => $object]);
345344

346-
$inverseObjects = unproxy($inverseObjects);
345+
$inverseObjects = unproxy($inverseObjects, withAutoRefresh: false);
347346

348347
// if the collection is indexed by a field, index the array
349348
if ($inverseRelationshipMetadata->collectionIndexedBy) {
@@ -379,9 +378,7 @@ protected function normalizeObject(object $object): object
379378
return $object;
380379
}
381380

382-
if ($object instanceof Proxy) {
383-
$object = $object->_real(withAutoRefresh: false);
384-
}
381+
$object = unproxy($object, withAutoRefresh: false);
385382

386383
$persistenceManager = $configuration->persistence();
387384
if (!$persistenceManager->hasPersistenceFor($object)) {
@@ -421,7 +418,7 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact
421418
}
422419
);
423420

424-
if (!Configuration::instance()->hasEventDispatcher()) {
421+
if (!Configuration::isBooted() || !Configuration::instance()->hasEventDispatcher()) {
425422
return $factory;
426423
}
427424

@@ -456,4 +453,13 @@ private function throwIfCannotCreateObject(): void
456453

457454
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));
458455
}
456+
457+
private function isPersistenceEnabled(): bool
458+
{
459+
try {
460+
return Configuration::instance()->isPersistenceEnabled();
461+
} catch (FoundryNotBooted) {
462+
return false;
463+
}
464+
}
459465
}

src/Persistence/ProxyGenerator.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,18 @@ public static function wrapFactory(PersistentProxyObjectFactory $factory, callab
6666
*
6767
* @return T
6868
*/
69-
public static function unwrap(mixed $what): mixed
69+
public static function unwrap(mixed $what, bool $withAutoRefresh = true): mixed
7070
{
7171
if (\is_array($what)) {
72-
return \array_map(self::unwrap(...), $what); // @phpstan-ignore return.type
72+
return \array_map(static fn(mixed $w) => self::unwrap($w, $withAutoRefresh), $what); // @phpstan-ignore return.type
7373
}
7474

7575
if (\is_string($what) && \is_a($what, Proxy::class, true)) {
7676
return \get_parent_class($what) ?: throw new \LogicException('Could not unwrap proxy.'); // @phpstan-ignore return.type
7777
}
7878

7979
if ($what instanceof Proxy) {
80-
return $what->_real(); // @phpstan-ignore return.type
80+
return $what->_real($withAutoRefresh); // @phpstan-ignore return.type
8181
}
8282

8383
return $what;

src/Persistence/functions.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ function proxy(object $object): object
107107
*
108108
* @return T
109109
*/
110-
function unproxy(mixed $what): mixed
110+
function unproxy(mixed $what, bool $withAutoRefresh = true): mixed
111111
{
112-
return ProxyGenerator::unwrap($what);
112+
return ProxyGenerator::unwrap($what, $withAutoRefresh);
113113
}
114114

115115
/**

tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPUnit\Framework\Attributes\DataProvider;
2020
use Psr\Cache\CacheItemPoolInterface;
2121
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
22+
use Zenstruck\Foundry\Configuration;
2223
use Zenstruck\Foundry\Persistence\PersistenceManager;
2324
use Zenstruck\Foundry\Tests\Integration\RequiresORM;
2425

@@ -123,6 +124,8 @@ public static function provideCascadeRelationshipsCombinations(): iterable
123124
}
124125

125126
yield from DoctrineCascadeRelationshipMetadata::allCombinations($relationshipFields);
127+
128+
Configuration::shutdown();
126129
}
127130

128131
public static function setCurrentProvidedMethodName(string $methodName): void

tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php

+31
Original file line numberDiff line numberDiff line change
@@ -323,11 +323,21 @@ public function disabling_persistence_cascades_to_children(): void
323323
foreach ($contact->getTags() as $tag) {
324324
$this->assertNull($tag->id);
325325
}
326+
}
326327

328+
/** @test */
329+
#[Test]
330+
#[DataProvider('provideCascadeRelationshipsCombinations')]
331+
#[UsingRelationships(Contact::class, ['category'])]
332+
public function disabling_persistence_cascades_to_children_one_to_many(): void
333+
{
327334
$category = static::categoryFactory()->withoutPersisting()->create([
328335
'contacts' => static::contactFactory()->many(3),
329336
]);
330337

338+
// ensure nothing was persisted in Doctrine by flushing
339+
self::getContainer()->get(EntityManagerInterface::class)->flush(); // @phpstan-ignore method.notFound
340+
331341
static::contactFactory()::assert()->empty();
332342
static::categoryFactory()::assert()->empty();
333343

@@ -339,6 +349,27 @@ public function disabling_persistence_cascades_to_children(): void
339349
}
340350
}
341351

352+
/** @test */
353+
#[Test]
354+
#[DataProvider('provideCascadeRelationshipsCombinations')]
355+
#[UsingRelationships(Contact::class, ['address'])]
356+
public function disabling_persistence_cascades_to_children_inversed_one_to_one(): void
357+
{
358+
$address = static::addressFactory()->withoutPersisting()->create([
359+
'contact' => static::contactFactory(),
360+
]);
361+
362+
// ensure nothing was persisted in Doctrine by flushing
363+
self::getContainer()->get(EntityManagerInterface::class)->flush(); // @phpstan-ignore method.notFound
364+
365+
static::contactFactory()::assert()->empty();
366+
static::addressFactory()::assert()->empty();
367+
368+
$this->assertNull($address->id);
369+
$this->assertInstanceOf(Contact::class, $address->getContact());
370+
$this->assertNull($address->getContact()->id);
371+
}
372+
342373
/** @test */
343374
#[Test]
344375
#[DataProvider('provideCascadeRelationshipsCombinations')]

tests/Unit/Persistence/PersistentObjectFactoryTest.php

+30
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111

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

14+
use PHPUnit\Framework\Attributes\DataProvider;
1415
use PHPUnit\Framework\Attributes\Test;
1516
use PHPUnit\Framework\TestCase;
17+
use Zenstruck\Foundry\FactoryCollection;
1618
use Zenstruck\Foundry\Test\Factories;
1719
use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity;
1820
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory;
@@ -62,4 +64,32 @@ public function random_or_create(): void
6264

6365
$this->assertSame('foo', $entity->getProp1());
6466
}
67+
68+
/**
69+
* @test
70+
* @dataProvider factoryCollectionDataProvider
71+
* @param FactoryCollection<GenericEntity, GenericEntityFactory> $collection
72+
*/
73+
#[Test] // @phpstan-ignore generics.notSubtype
74+
#[DataProvider('factoryCollectionDataProvider')]
75+
public function can_use_factory_collection_methods_in_data_providers(FactoryCollection $collection): void
76+
{
77+
self::assertEquals(
78+
[
79+
new GenericEntity('foo'),
80+
],
81+
$collection->create(),
82+
);
83+
}
84+
85+
public static function factoryCollectionDataProvider(): iterable
86+
{
87+
yield [
88+
GenericEntityFactory::new()->sequence([
89+
[
90+
'prop1' => 'foo',
91+
],
92+
]),
93+
];
94+
}
6595
}

0 commit comments

Comments
 (0)