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 0fbfdee

Browse files
committedMar 24, 2025··
fix: introduce so-called transactions
1 parent a4d38a8 commit 0fbfdee

12 files changed

+313
-99
lines changed
 

‎src/Factory.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ protected function normalizeParameter(string $field, mixed $value): mixed
235235
);
236236
}
237237

238-
return \is_object($value) ? $this->normalizeObject($value) : $value;
238+
return \is_object($value) ? $this->normalizeObject($field, $value) : $value;
239239
}
240240

241241
/**
@@ -253,7 +253,7 @@ protected function normalizeCollection(string $field, FactoryCollection $collect
253253
/**
254254
* @internal
255255
*/
256-
protected function normalizeObject(object $object): object
256+
protected function normalizeObject(string $field, object $object): object
257257
{
258258
return $object;
259259
}

‎src/ORM/OrmV2PersistenceStrategy.php

+20-28
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
use Doctrine\ORM\Mapping\ClassMetadataInfo;
1717
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
1818
use Doctrine\Persistence\Mapping\MappingException;
19-
use Zenstruck\Foundry\Persistence\InverseRelationshipMetadata;
19+
use Zenstruck\Foundry\Persistence\Relationship\OneToManyRelationship;
20+
use Zenstruck\Foundry\Persistence\Relationship\OneToOneRelationship;
21+
use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata;
2022

2123
/**
2224
* @internal
@@ -25,49 +27,39 @@
2527
*/
2628
final class OrmV2PersistenceStrategy extends AbstractORMPersistenceStrategy
2729
{
28-
public function inversedRelationshipMetadata(string $parent, string $child, string $field): ?InverseRelationshipMetadata
30+
public function inversedRelationshipMetadata(string $parent, string $child, string $field): ?RelationshipMetadata
2931
{
30-
$metadata = $this->classMetadata($child);
32+
$associationMapping = $this->getAssociationMapping($parent, $child, $field);
3133

32-
$inversedAssociation = $this->getAssociationMapping($parent, $child, $field);
33-
34-
if (null === $inversedAssociation || !$metadata instanceof ClassMetadataInfo) {
34+
if (null === $associationMapping) {
3535
return null;
3636
}
3737

3838
if (!\is_a(
3939
$child,
40-
$inversedAssociation['targetEntity'],
40+
$associationMapping['targetEntity'],
4141
allow_string: true
4242
)) { // is_a() handles inheritance as well
4343
throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]");
4444
}
4545

46-
// exclude "owning" side of the association (owning OneToOne or ManyToOne)
47-
if (!\in_array(
48-
$inversedAssociation['type'],
49-
[ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::ONE_TO_ONE],
50-
true
51-
)
52-
|| !isset($inversedAssociation['mappedBy'])
53-
) {
54-
return null;
55-
}
46+
$inverseField = $associationMapping['isOwningSide'] ? $associationMapping['inversedBy'] ?? null : $associationMapping['mappedBy'] ?? null;
5647

57-
$association = $metadata->getAssociationMapping($inversedAssociation['mappedBy']);
58-
59-
// only keep *ToOne associations
60-
if (!$metadata->isSingleValuedAssociation($association['fieldName'])) {
48+
if (null === $inverseField) {
6149
return null;
6250
}
6351

64-
$inversedAssociationMetadata = $this->classMetadata($inversedAssociation['sourceEntity']);
65-
66-
return new InverseRelationshipMetadata(
67-
inverseField: $association['fieldName'],
68-
isCollection: $inversedAssociationMetadata->isCollectionValuedAssociation($inversedAssociation['fieldName']),
69-
collectionIndexedBy: $inversedAssociation['indexBy'] ?? null
70-
);
52+
return match (true) {
53+
ClassMetadataInfo::ONE_TO_MANY === $associationMapping['type'] => new OneToManyRelationship(
54+
inverseField: $inverseField,
55+
collectionIndexedBy: $associationMapping['indexBy'] ?? null
56+
),
57+
ClassMetadataInfo::ONE_TO_ONE === $associationMapping['type'] => new OneToOneRelationship(
58+
inverseField: $inverseField,
59+
isOwning: $associationMapping['isOwningSide'] ?? false
60+
),
61+
default => null,
62+
};
7163
}
7264

7365
/**

‎src/ORM/OrmV3PersistenceStrategy.php

+22-23
Original file line numberDiff line numberDiff line change
@@ -14,50 +14,49 @@
1414
namespace Zenstruck\Foundry\ORM;
1515

1616
use Doctrine\ORM\Mapping\AssociationMapping;
17-
use Doctrine\ORM\Mapping\ClassMetadata;
18-
use Doctrine\ORM\Mapping\InverseSideMapping;
1917
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
20-
use Doctrine\ORM\Mapping\ToManyAssociationMapping;
18+
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
19+
use Doctrine\ORM\Mapping\OneToOneAssociationMapping;
2120
use Doctrine\Persistence\Mapping\MappingException;
22-
use Zenstruck\Foundry\Persistence\InverseRelationshipMetadata;
21+
use Zenstruck\Foundry\Persistence\Relationship\OneToManyRelationship;
22+
use Zenstruck\Foundry\Persistence\Relationship\OneToOneRelationship;
23+
use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata;
2324

2425
final class OrmV3PersistenceStrategy extends AbstractORMPersistenceStrategy
2526
{
26-
public function inversedRelationshipMetadata(string $parent, string $child, string $field): ?InverseRelationshipMetadata
27+
public function inversedRelationshipMetadata(string $parent, string $child, string $field): ?RelationshipMetadata
2728
{
28-
$metadata = $this->classMetadata($child);
29+
$associationMapping = $this->getAssociationMapping($parent, $child, $field);
2930

30-
$inversedAssociation = $this->getAssociationMapping($parent, $child, $field);
31-
32-
if (null === $inversedAssociation || !$metadata instanceof ClassMetadata) {
31+
if (null === $associationMapping) {
3332
return null;
3433
}
3534

3635
if (!\is_a(
3736
$child,
38-
$inversedAssociation->targetEntity,
37+
$associationMapping->targetEntity,
3938
allow_string: true
4039
)) { // is_a() handles inheritance as well
4140
throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]");
4241
}
4342

44-
// exclude "owning" side of the association (owning OneToOne or ManyToOne)
45-
if (!$inversedAssociation instanceof InverseSideMapping) {
46-
return null;
47-
}
48-
49-
$association = $metadata->getAssociationMapping($inversedAssociation->mappedBy);
43+
$inverseField = $associationMapping->isOwningSide() ? $associationMapping->inversedBy : $associationMapping->mappedBy;
5044

51-
// only keep *ToOne associations
52-
if (!$metadata->isSingleValuedAssociation($association->fieldName)) {
45+
if (null === $inverseField) {
5346
return null;
5447
}
5548

56-
return new InverseRelationshipMetadata(
57-
inverseField: $association->fieldName,
58-
isCollection: $inversedAssociation instanceof ToManyAssociationMapping,
59-
collectionIndexedBy: $inversedAssociation->isIndexed() ? $inversedAssociation->indexBy() : null
60-
);
49+
return match (true) {
50+
$associationMapping instanceof OneToManyAssociationMapping => new OneToManyRelationship(
51+
inverseField: $inverseField,
52+
collectionIndexedBy: $associationMapping->isIndexed() ? $associationMapping->indexBy() : null
53+
),
54+
$associationMapping instanceof OneToOneAssociationMapping => new OneToOneRelationship(
55+
inverseField: $inverseField,
56+
isOwning: $associationMapping->isOwningSide()
57+
),
58+
default => null,
59+
};
6160
}
6261

6362
/**

‎src/Persistence/PersistenceManager.php

+20-19
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Zenstruck\Foundry\ORM\AbstractORMPersistenceStrategy;
2020
use Zenstruck\Foundry\Persistence\Exception\NoPersistenceStrategy;
2121
use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed;
22+
use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata;
2223
use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager;
2324

2425
/**
@@ -31,12 +32,14 @@ final class PersistenceManager
3132
private bool $flush = true;
3233
private bool $persist = true;
3334

34-
/** @var array<int, list<object>> */
35+
/** @var list<object> */
3536
private array $objectsToPersist = [];
3637

37-
/** @var array<int, list<callable():void>> */
38+
/** @var list<callable():void> */
3839
private array $afterPersistCallbacks = [];
3940

41+
private bool $transactionStarted = false;
42+
4043
/**
4144
* @param iterable<PersistenceStrategy> $strategies
4245
*/
@@ -90,19 +93,21 @@ public function save(object $object): object
9093
*/
9194
public function startTransaction(): void
9295
{
93-
$this->objectsToPersist[] = [];
94-
$this->afterPersistCallbacks[] = [];
96+
$this->transactionStarted = true;
97+
}
98+
99+
public function isTransactionStarted(): bool
100+
{
101+
return $this->transactionStarted;
95102
}
96103

97104
public function commit(): void
98105
{
99106
$objectManagers = [];
100107

101-
$objectsToPersist = \array_pop($this->objectsToPersist);
102-
103-
if (null === $objectsToPersist) {
104-
return;
105-
}
108+
$objectsToPersist = $this->objectsToPersist;
109+
$this->objectsToPersist = [];
110+
$this->transactionStarted = false;
106111

107112
foreach ($objectsToPersist as $object) {
108113
$om = $this->strategyFor($object::class)->objectManagerFor($object::class);
@@ -134,15 +139,10 @@ public function scheduleForInsert(object $object, array $afterPersistCallbacks =
134139
$object = unproxy($object);
135140
}
136141

137-
if (0 === \count($this->objectsToPersist)) {
138-
throw new \LogicException('No transaction started yet.');
139-
}
140-
141-
$transactionCount = \count($this->objectsToPersist) - 1;
142-
$this->objectsToPersist[$transactionCount][] = $object;
142+
$this->objectsToPersist[] = $object;
143143

144-
$this->afterPersistCallbacks[$transactionCount] = [
145-
...$this->afterPersistCallbacks[$transactionCount],
144+
$this->afterPersistCallbacks = [
145+
...$this->afterPersistCallbacks,
146146
...$afterPersistCallbacks,
147147
];
148148

@@ -305,7 +305,7 @@ public function repositoryFor(string $class): ObjectRepository
305305
* @param class-string $parent
306306
* @param class-string $child
307307
*/
308-
public function inverseRelationshipMetadata(string $parent, string $child, string $field): ?InverseRelationshipMetadata
308+
public function inverseRelationshipMetadata(string $parent, string $child, string $field): ?RelationshipMetadata
309309
{
310310
$parent = unproxy($parent);
311311
$child = unproxy($child);
@@ -412,7 +412,8 @@ private function callPostPersistCallbacks(): void
412412
return;
413413
}
414414

415-
$afterPersistCallbacks = \array_pop($this->afterPersistCallbacks);
415+
$afterPersistCallbacks = $this->afterPersistCallbacks;
416+
$this->afterPersistCallbacks = [];
416417

417418
foreach ($afterPersistCallbacks as $afterPersistCallback) {
418419
$afterPersistCallback();

‎src/Persistence/PersistenceStrategy.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Doctrine\Persistence\Mapping\ClassMetadata;
1616
use Doctrine\Persistence\Mapping\MappingException;
1717
use Doctrine\Persistence\ObjectManager;
18+
use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata;
1819

1920
/**
2021
* @author Kevin Bond <kevinbond@gmail.com>
@@ -63,7 +64,7 @@ public function objectManagers(): array
6364
* @param class-string $parent
6465
* @param class-string $child
6566
*/
66-
public function inversedRelationshipMetadata(string $parent, string $child, string $field): ?InverseRelationshipMetadata
67+
public function inversedRelationshipMetadata(string $parent, string $child, string $field): ?RelationshipMetadata
6768
{
6869
return null;
6970
}

‎src/Persistence/PersistentObjectFactory.php

+44-15
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
use Zenstruck\Foundry\ObjectFactory;
2323
use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects;
2424
use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed;
25+
use Zenstruck\Foundry\Persistence\Relationship\OneToManyRelationship;
26+
use Zenstruck\Foundry\Persistence\Relationship\OneToOneRelationship;
2527

2628
use function Zenstruck\Foundry\get;
2729
use function Zenstruck\Foundry\set;
@@ -198,7 +200,10 @@ final public static function truncate(): void
198200
*/
199201
public function create(callable|array $attributes = []): object
200202
{
201-
if (PersistMode::PERSIST === $this->persistMode() && $this->isRootFactory) {
203+
$transactionStarted = false;
204+
205+
if (Configuration::isBooted() && PersistMode::PERSIST === $this->persistMode() && $this->isRootFactory) {
206+
$transactionStarted = Configuration::instance()->persistence()->isTransactionStarted();
202207
Configuration::instance()->persistence()->startTransaction();
203208
}
204209

@@ -212,7 +217,7 @@ public function create(callable|array $attributes = []): object
212217

213218
$this->throwIfCannotCreateObject();
214219

215-
if (PersistMode::PERSIST !== $this->persistMode() || !$this->isRootFactory) {
220+
if ($transactionStarted || PersistMode::PERSIST !== $this->persistMode() || !$this->isRootFactory) {
216221
return $object;
217222
}
218223

@@ -296,11 +301,11 @@ protected function normalizeParameter(string $field, mixed $value): mixed
296301
if ($value instanceof self) {
297302
$pm = Configuration::instance()->persistence();
298303

299-
$inversedRelationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $value::class(), $field);
304+
$relationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $value::class(), $field);
300305

301306
// handle inversed OneToOne
302-
if ($inversedRelationshipMetadata && !$inversedRelationshipMetadata->isCollection) {
303-
$inverseField = $inversedRelationshipMetadata->inverseField;
307+
if ($relationshipMetadata instanceof OneToOneRelationship && !$relationshipMetadata->isOwning) {
308+
$inverseField = $relationshipMetadata->inverseField();
304309

305310
// we need to handle the circular dependency involved by inversed one-to-one relationship:
306311
// a placeholder object is used, which will be replaced by the real object, after its instantiation
@@ -337,9 +342,9 @@ protected function normalizeCollection(string $field, FactoryCollection $collect
337342

338343
$inverseRelationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $collection->factory::class(), $field);
339344

340-
if ($inverseRelationshipMetadata && $inverseRelationshipMetadata->isCollection) {
345+
if ($inverseRelationshipMetadata instanceof OneToManyRelationship) {
341346
$this->tempAfterInstantiate[] = function(object $object) use ($collection, $inverseRelationshipMetadata, $field) {
342-
$inverseField = $inverseRelationshipMetadata->inverseField;
347+
$inverseField = $inverseRelationshipMetadata->inverseField();
343348

344349
$inverseObjects = $collection->withPersistMode(
345350
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
@@ -371,24 +376,39 @@ protected function normalizeCollection(string $field, FactoryCollection $collect
371376
*
372377
* @internal
373378
*/
374-
protected function normalizeObject(object $object): object
379+
protected function normalizeObject(string $field, object $object): object
375380
{
376381
$configuration = Configuration::instance();
377382

378-
if (
379-
!$this->isPersisting()
380-
|| !$configuration->isPersistenceAvailable()
381-
) {
383+
$object = unproxy($object, withAutoRefresh: false);
384+
385+
if (!$configuration->isPersistenceAvailable()) {
382386
return $object;
383387
}
384388

385-
$object = unproxy($object, withAutoRefresh: false);
386-
387389
$persistenceManager = $configuration->persistence();
390+
388391
if (!$persistenceManager->hasPersistenceFor($object)) {
389392
return $object;
390393
}
391394

395+
$inverseRelationship = $persistenceManager->inverseRelationshipMetadata(static::class(), $object::class, $field);
396+
397+
if ($inverseRelationship instanceof OneToOneRelationship) {
398+
$this->tempAfterInstantiate[] = static function(object $newObject) use ($object, $inverseRelationship) {
399+
try {
400+
set($object, $inverseRelationship->inverseField(), $newObject);
401+
} catch (\Throwable) {
402+
}
403+
};
404+
}
405+
406+
if (
407+
!$this->isPersisting()
408+
) {
409+
return $object;
410+
}
411+
392412
if (!$persistenceManager->isPersisted($object)) {
393413
$persistenceManager->scheduleForInsert($object);
394414

@@ -435,7 +455,16 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact
435455

436456
Configuration::instance()->persistence()->scheduleForInsert($object, $afterPersistCallbacks);
437457
}
438-
);
458+
)
459+
->afterPersist(
460+
static function(object $object): void {
461+
try {
462+
Configuration::instance()->persistence()->refresh($object);
463+
} catch (RefreshObjectFailed) {
464+
}
465+
}
466+
)
467+
;
439468
}
440469

441470
private function throwIfCannotCreateObject(): void
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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\Persistence\Relationship;
15+
16+
/**
17+
* @author Nicolas PHILIPPE <nikophil@gmail.com>
18+
*
19+
* @internal
20+
*/
21+
final class OneToManyRelationship implements RelationshipMetadata
22+
{
23+
public function __construct(
24+
private readonly string $inverseField,
25+
public readonly ?string $collectionIndexedBy,
26+
) {
27+
}
28+
29+
public function inverseField(): string
30+
{
31+
return $this->inverseField;
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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\Persistence\Relationship;
15+
16+
/**
17+
* @author Nicolas PHILIPPE <nikophil@gmail.com>
18+
*
19+
* @internal
20+
*/
21+
final class OneToOneRelationship implements RelationshipMetadata
22+
{
23+
public function __construct(
24+
private readonly string $inverseField,
25+
public readonly bool $isOwning,
26+
) {
27+
}
28+
29+
public function inverseField(): string
30+
{
31+
return $this->inverseField;
32+
}
33+
}

‎src/Persistence/InverseRelationshipMetadata.php ‎src/Persistence/Relationship/RelationshipMetadata.php

+3-8
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,14 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Zenstruck\Foundry\Persistence;
12+
namespace Zenstruck\Foundry\Persistence\Relationship;
1313

1414
/**
1515
* @author Kevin Bond <kevinbond@gmail.com>
1616
*
1717
* @internal
1818
*/
19-
final class InverseRelationshipMetadata
19+
interface RelationshipMetadata
2020
{
21-
public function __construct(
22-
public readonly string $inverseField,
23-
public readonly bool $isCollection,
24-
public readonly ?string $collectionIndexedBy,
25-
) {
26-
}
21+
public function inverseField(): string;
2722
}

‎tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,18 @@ class InverseSide extends Base
2525
{
2626
public function __construct(
2727
#[ORM\OneToOne(mappedBy: 'inverseSide')] // @phpstan-ignore doctrine.associationType
28-
public OwningSide $owningSide,
28+
private OwningSide $owningSide,
2929
) {
3030
}
31+
32+
public function getOwningSide(): OwningSide
33+
{
34+
return $this->owningSide;
35+
}
36+
37+
public function setOwningSide(OwningSide $owningSide): void
38+
{
39+
$this->owningSide = $owningSide;
40+
$owningSide->inverseSide = $this;
41+
}
3142
}

‎tests/Integration/ORM/EdgeCasesRelationshipTest.php

+55-2
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public function inverse_one_to_one_with_non_nullable_inverse_side(): void
7777
$owningSideFactory::assert()->count(1);
7878
$inverseSideFactory::assert()->count(1);
7979

80-
self::assertSame($inverseSide, $inverseSide->owningSide->inverseSide);
80+
self::assertSame($inverseSide, $inverseSide->getOwningSide()->inverseSide);
8181
}
8282

8383
/** @test */
@@ -210,7 +210,10 @@ public function inverse_one_to_one_with_custom_id(): void
210210

211211
/** @test */
212212
#[Test]
213-
public function object_with_union_type(): void
213+
#[DataProvider('provideCascadeRelationshipsCombinations')]
214+
#[UsingRelationships(OneToManyWithUnionType\OwningSideEntity::class, ['item'])]
215+
#[RequiresPhpunit('>=11.4')]
216+
public function after_instantiate_flushing_using_current_object_in_relationship_one_to_one(): void
214217
{
215218
$owningSideFactory = persistent_factory(OneToManyWithUnionType\OwningSideEntity::class);
216219
$hasOneToManyWithUnionTypeFactory = persistent_factory(OneToManyWithUnionType\HasOneToManyWithUnionType::class);
@@ -228,6 +231,56 @@ public function object_with_union_type(): void
228231
self::assertInstanceOf(Collection::class, $object->collection);
229232
}
230233

234+
/** @test */
235+
#[Test]
236+
#[DataProvider('provideCascadeRelationshipsCombinations')]
237+
#[UsingRelationships(InversedOneToOneWithNonNullableOwning\OwningSide::class, ['inverseSide'])]
238+
#[RequiresPhpunit('>=11.4')]
239+
public function after_instantiate_flushing_using_current_object_in_relationship_inversed_one_to_one(): void
240+
{
241+
$owningSideFactory = persistent_factory(InversedOneToOneWithNonNullableOwning\OwningSide::class);
242+
$inverseSideFactory = persistent_factory(InversedOneToOneWithNonNullableOwning\InverseSide::class);
243+
244+
$owningSide = $owningSideFactory
245+
->afterInstantiate(
246+
static function(InversedOneToOneWithNonNullableOwning\OwningSide $o) use ($inverseSideFactory) {
247+
$inverseSideFactory->create(['owningSide' => $o]);
248+
}
249+
)
250+
->create();
251+
252+
$owningSideFactory::assert()->count(1);
253+
$inverseSideFactory::assert()->count(1);
254+
255+
self::assertNotNull($owningSide->inverseSide);
256+
self::assertSame($owningSide, $owningSide->inverseSide->getOwningSide());
257+
}
258+
259+
/** @test */
260+
#[Test]
261+
#[DataProvider('provideCascadeRelationshipsCombinations')]
262+
#[UsingRelationships(InversedOneToOneWithNonNullableOwning\OwningSide::class, ['inverseSide'])]
263+
#[RequiresPhpunit('>=11.4')]
264+
public function can_create_one_to_one(): void
265+
{
266+
$owningSideFactory = persistent_factory(InversedOneToOneWithNonNullableOwning\OwningSide::class);
267+
$inverseSideFactory = persistent_factory(InversedOneToOneWithNonNullableOwning\InverseSide::class);
268+
269+
$owningSide = $owningSideFactory
270+
->afterInstantiate(
271+
static function(InversedOneToOneWithNonNullableOwning\OwningSide $o) use ($inverseSideFactory): void {
272+
$inverseSideFactory->create(['owningSide' => $o]);
273+
}
274+
)
275+
->create();
276+
277+
$owningSideFactory::assert()->count(1);
278+
$inverseSideFactory::assert()->count(1);
279+
280+
self::assertNotNull($owningSide->inverseSide);
281+
self::assertSame($owningSide, $owningSide->inverseSide->getOwningSide());
282+
}
283+
231284
/**
232285
* @test
233286
*/

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

+67
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,8 @@ public function it_uses_after_persist_with_inversed_one_to_one(): void
506506

507507
/** @test */
508508
#[Test]
509+
#[DataProvider('provideCascadeRelationshipsCombinations')]
510+
#[UsingRelationships(Contact::class, ['category'])]
509511
public function can_call_create_in_after_persist_callback(): void
510512
{
511513
$category = static::categoryFactory()::new()
@@ -522,6 +524,8 @@ public function can_call_create_in_after_persist_callback(): void
522524

523525
/** @test */
524526
#[Test]
527+
#[DataProvider('provideCascadeRelationshipsCombinations')]
528+
#[UsingRelationships(Contact::class, ['address'])]
525529
public function can_use_nested_after_persist_callback(): void
526530
{
527531
$contact = static::contactFactory()::createOne(
@@ -538,6 +542,8 @@ public function can_use_nested_after_persist_callback(): void
538542

539543
/** @test */
540544
#[Test]
545+
#[DataProvider('provideCascadeRelationshipsCombinations')]
546+
#[UsingRelationships(Contact::class, ['category'])]
541547
public function can_call_create_in_nested_after_persist_callback(): void
542548
{
543549
$contact = static::contactFactory()::createOne(
@@ -605,6 +611,67 @@ public function inverse_one_to_one_with_lazy_flush(): void
605611
self::assertNotNull($address->getContact()->getCategory());
606612
}
607613

614+
/** @test */
615+
#[Test]
616+
#[DataProvider('provideCascadeRelationshipsCombinations')]
617+
#[UsingRelationships(Contact::class, ['category'])]
618+
public function after_instantiate_flushing_using_current_object_in_relationship_many_to_one(): void
619+
{
620+
$category = static::categoryFactory()
621+
->afterInstantiate(
622+
static function(Category $c): void {
623+
static::contactFactory()->create(['category' => $c]);
624+
}
625+
)
626+
->create();
627+
628+
static::contactFactory()::assert()->count(1);
629+
static::categoryFactory()::assert()->count(1);
630+
631+
self::assertCount(1, $category->getContacts());
632+
self::assertNotNull($category->getContacts()[0] ?? null);
633+
}
634+
635+
/** @test */
636+
#[Test]
637+
#[DataProvider('provideCascadeRelationshipsCombinations')]
638+
#[UsingRelationships(Contact::class, ['category'])]
639+
public function after_instantiate_flushing_using_current_object_in_relationship_one_to_many(): void
640+
{
641+
$contact = static::contactFactory()
642+
->afterInstantiate(
643+
static function(Contact $c): void {
644+
static::categoryFactory()->create(['contacts' => [$c]]);
645+
}
646+
)
647+
->create(['category' => null]);
648+
649+
static::contactFactory()::assert()->count(1);
650+
static::categoryFactory()::assert()->count(1);
651+
652+
self::assertNotNull($contact->getCategory());
653+
self::assertCount(1, $contact->getCategory()->getContacts());
654+
}
655+
656+
/** @test */
657+
#[Test]
658+
#[DataProvider('provideCascadeRelationshipsCombinations')]
659+
#[UsingRelationships(Contact::class, ['address'])]
660+
public function after_instantiate_flushing_using_current_object_in_relationship_one_to_one(): void
661+
{
662+
$address = static::addressFactory()
663+
->afterInstantiate(
664+
static function(Address $a): void {
665+
static::contactFactory()->create(['address' => $a]);
666+
}
667+
)->create();
668+
669+
static::contactFactory()::assert()->count(1);
670+
static::addressFactory()::assert()->count(1);
671+
672+
self::assertNotNull($address->getContact());
673+
}
674+
608675
/** @return PersistentObjectFactory<Contact> */
609676
protected static function contactFactoryWithoutCategory(): PersistentObjectFactory
610677
{

0 commit comments

Comments
 (0)
Please sign in to comment.