Skip to content

Commit 2173a48

Browse files
committed
fix: one to one no more uses placeholder
1 parent b23f1d3 commit 2173a48

File tree

11 files changed

+395
-34
lines changed

11 files changed

+395
-34
lines changed

Diff for: src/Persistence/PersistenceManager.php

-11
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,6 @@ public function scheduleForInsert(object $object, array $afterPersistCallbacks =
111111
return $object;
112112
}
113113

114-
public function forget(object $object): void
115-
{
116-
if ($this->isPersisted($object)) {
117-
throw new \LogicException('Cannot forget an object already persisted.');
118-
}
119-
120-
$om = $this->strategyFor($object::class)->objectManagerFor($object::class);
121-
122-
$om->detach($object);
123-
}
124-
125114
/**
126115
* @template T
127116
*

Diff for: src/Persistence/PersistentObjectFactory.php

+29-18
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects;
2424
use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed;
2525

26+
use function Zenstruck\Foundry\force;
2627
use function Zenstruck\Foundry\get;
2728
use function Zenstruck\Foundry\set;
2829

@@ -290,30 +291,40 @@ protected function normalizeParameter(string $field, mixed $value): mixed
290291
if ($value instanceof self) {
291292
$pm = Configuration::instance()->persistence();
292293

293-
$inversedRelationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $value::class(), $field);
294+
$relationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $value::class(), $field);
294295

295-
// handle inversed OneToOne
296-
if ($inversedRelationshipMetadata && !$inversedRelationshipMetadata->isCollection) {
297-
$inverseField = $inversedRelationshipMetadata->inverseField;
296+
// handle inverse OneToOne
297+
if ($relationshipMetadata && !$relationshipMetadata->isCollection) {
298+
$inverseField = $relationshipMetadata->inverseField;
298299

299-
$inversedObject = $value->withPersistMode(
300+
$value = $value->withPersistMode(
300301
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
301-
)
302+
);
303+
304+
if ((new \ReflectionClass(static::class()))->getProperty($field)->getType()?->allowsNull()) {
305+
$this->tempAfterInstantiate[] = static function(object $object) use ($value, $inverseField, $field) {
306+
$inverseObject = $value->create([$inverseField => $object]);
302307

303-
// we need to handle the circular dependency involved by inversed one-to-one relationship:
304-
// a placeholder object is used, which will be replaced by the real object, after its instantiation
305-
->create([
306-
$inverseField => $placeholder = (new \ReflectionClass(static::class()))->newInstanceWithoutConstructor(),
307-
]);
308+
set($object, $field, unproxy($inverseObject, withAutoRefresh: false));
309+
};
308310

309-
$inversedObject = unproxy($inversedObject, withAutoRefresh: false);
311+
// we're using "force" here to avoid a potential type check in a setter
312+
return force(null);
313+
} elseif ((new \ReflectionClass($value::class()))->getProperty($inverseField)->getType()?->allowsNull()) {
314+
$inverseObject = unproxy(
315+
// we're using "force" here to avoid a potential type check in a setter
316+
$value->create([$inverseField => force(null)]),
317+
withAutoRefresh: false
318+
);
310319

311-
$this->tempAfterInstantiate[] = static function(object $object) use ($inversedObject, $inverseField, $pm, $placeholder) {
312-
$pm->forget($placeholder);
313-
set($inversedObject, $inverseField, $object);
314-
};
320+
$this->tempAfterInstantiate[] = static function(object $object) use ($inverseObject, $inverseField) {
321+
set($inverseObject, $inverseField, $object);
322+
};
315323

316-
return $inversedObject;
324+
return $inverseObject;
325+
} else {
326+
throw new \InvalidArgumentException('Cannot handle inverse OneToOne relationship because both side are not nullable, which will result in a circular dependency.');
327+
}
317328
}
318329
}
319330

@@ -352,7 +363,7 @@ protected function normalizeCollection(string $field, FactoryCollection $collect
352363
set($object, $field, $inverseObjects);
353364
};
354365

355-
// creation delegated to afterPersist hook - return empty array here
366+
// creation delegated to tempAfterInstantiate hook - return empty array here
356367
return [];
357368
}
358369

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\InversedOneToOneWithManyToOne;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
use Zenstruck\Foundry\Tests\Fixture\Model\Base;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <[email protected]>
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 <[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\InversedOneToOneWithManyToOne;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
use Zenstruck\Foundry\Tests\Fixture\Model\Base;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <[email protected]>
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 <[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\InversedOneToOneWithManyToOne;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
use Zenstruck\Foundry\Tests\Fixture\Model\Base;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <[email protected]>
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+
}

Diff for: 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
}
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+
}

Diff for: tests/Integration/ORM/EdgeCasesRelationshipTest.php

+55-4
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangesEntityRelationshipCascadePersist;
2424
use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships;
2525
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\IndexedOneToMany;
26+
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithManyToOne;
2627
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning;
2728
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany;
29+
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithoutAutoGeneratedId;
2830
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithSetter;
2931
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing;
3032
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\OneToManyWithUnionType;
@@ -75,7 +77,7 @@ public function inverse_one_to_one_with_non_nullable_inverse_side(): void
7577
$owningSideFactory::assert()->count(1);
7678
$inverseSideFactory::assert()->count(1);
7779

78-
self::assertSame($inverseSide, $inverseSide->owningSide->inverseSide);
80+
self::assertSame($inverseSide, $inverseSide->getOwningSide()->inverseSide);
7981
}
8082

8183
/** @test */
@@ -101,7 +103,7 @@ public function inverse_one_to_one_with_both_nullable(): void
101103
#[DataProvider('provideCascadeRelationshipsCombinations')]
102104
#[UsingRelationships(InversedOneToOneWithOneToMany\OwningSide::class, ['inverseSide'])]
103105
#[UsingRelationships(InversedOneToOneWithOneToMany\Item::class, ['owningSide'])]
104-
#[RequiresPhpunit('^11.4')]
106+
#[RequiresPhpunit('>=11.4')]
105107
public function inverse_one_to_one_with_one_to_many(): void
106108
{
107109
$inverseSideFactory = persistent_factory(InversedOneToOneWithOneToMany\InverseSide::class);
@@ -142,7 +144,7 @@ public function many_to_many_to_self_referencing_inverse_side(): void
142144
#[Test]
143145
#[DataProvider('provideCascadeRelationshipsCombinations')]
144146
#[UsingRelationships(IndexedOneToMany\ParentEntity::class, ['items'])]
145-
#[RequiresPhpunit('^11.4')]
147+
#[RequiresPhpunit('>=11.4')]
146148
public function indexed_one_to_many(): void
147149
{
148150
$parentFactory = persistent_factory(IndexedOneToMany\ParentEntity::class);
@@ -162,7 +164,56 @@ public function indexed_one_to_many(): void
162164

163165
/** @test */
164166
#[Test]
165-
public function object_with_union_type(): void
167+
#[DataProvider('provideCascadeRelationshipsCombinations')]
168+
#[UsingRelationships(InversedOneToOneWithManyToOne\InverseSide::class, ['owningSide', 'item'])]
169+
#[RequiresPhpunit('>=11.4')]
170+
public function inversed_one_to_one_can_be_used_after_other_relationship(): void
171+
{
172+
$inverseSideFactory = persistent_factory(InversedOneToOneWithManyToOne\InverseSide::class);
173+
$owningSideFactory = persistent_factory(InversedOneToOneWithManyToOne\OwningSide::class);
174+
$itemFactory = persistent_factory(InversedOneToOneWithManyToOne\Item::class);
175+
176+
$inverseSide = $inverseSideFactory->create(
177+
[
178+
'mandatoryField' => 'foo',
179+
'owningSide' => $owningSideFactory,
180+
'item' => $itemFactory,
181+
]
182+
);
183+
184+
$inverseSideFactory::assert()->count(1);
185+
$owningSideFactory::assert()->count(1);
186+
$itemFactory::assert()->count(1);
187+
188+
self::assertNotNull($inverseSide->owningSide);
189+
self::assertSame($inverseSide, $inverseSide->owningSide->inverseSide);
190+
self::assertNotNull($inverseSide->item);
191+
}
192+
193+
/** @test */
194+
#[Test]
195+
#[DataProvider('provideCascadeRelationshipsCombinations')]
196+
#[UsingRelationships(InversedOneToOneWithoutAutoGeneratedId\OwningSide::class, ['inverseSide'])]
197+
#[RequiresPhpunit('>=11.4')]
198+
public function inverse_one_to_one_with_custom_id(): void
199+
{
200+
$owningSideFactory = persistent_factory(InversedOneToOneWithoutAutoGeneratedId\OwningSide::class);
201+
$inverseSideFactory = persistent_factory(InversedOneToOneWithoutAutoGeneratedId\InverseSide::class);
202+
203+
$inverseSide = $inverseSideFactory->create(['owningSide' => $owningSideFactory]);
204+
205+
$owningSideFactory::assert()->count(1);
206+
$inverseSideFactory::assert()->count(1);
207+
208+
self::assertSame($inverseSide, $inverseSide->owningSide->inverseSide);
209+
}
210+
211+
/** @test */
212+
#[Test]
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
166217
{
167218
$owningSideFactory = persistent_factory(OneToManyWithUnionType\OwningSideEntity::class);
168219
$hasOneToManyWithUnionTypeFactory = persistent_factory(OneToManyWithUnionType\HasOneToManyWithUnionType::class);

0 commit comments

Comments
 (0)