Skip to content

Commit 40ce8a2

Browse files
authored
fix: reuse should work with all kind of relationships (#915)
1 parent b6e1bbb commit 40ce8a2

File tree

6 files changed

+289
-22
lines changed

6 files changed

+289
-22
lines changed

src/Factory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ final protected function normalizeAttributes(array|callable $attributes = []): a
185185
// "reused" attributes will override the ones from "defaults()"
186186
// but should be overridden by the other states of the factory
187187
if ($this instanceof ObjectFactory) {
188-
$mergedAttributes[] = $this->reusedAttributes();
188+
$mergedAttributes[] = $this->normalizeReusedAttributes();
189189
}
190190

191191
$mergedAttributes = [...$mergedAttributes, ...$this->attributes, $attributes];

src/FactoryCollection.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,26 @@ public function distribute(string $field, array $values): self
212212
);
213213
}
214214

215+
/**
216+
* @internal
217+
*/
218+
public function reuse(object ...$objects): static
219+
{
220+
if (0 === \count($objects)) {
221+
return $this;
222+
}
223+
224+
$factories = $this->all();
225+
226+
return new self(
227+
$this->factory,
228+
static fn() => \array_map(
229+
static fn(Factory $f) => $f instanceof ObjectFactory ? $f->reuse(...$objects) : $f,
230+
$factories,
231+
)
232+
);
233+
}
234+
215235
public function getIterator(): \Traversable
216236
{
217237
return new \ArrayIterator($this->all());

src/ObjectFactory.php

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use Zenstruck\Foundry\Object\Instantiator;
1515

16+
use function Zenstruck\Foundry\Persistence\unproxy;
17+
1618
/**
1719
* @author Kevin Bond <[email protected]>
1820
*
@@ -33,7 +35,7 @@ abstract class ObjectFactory extends Factory
3335
/** @phpstan-var InstantiatorCallable|null */
3436
private $instantiator;
3537

36-
/** @phpstan-var array<class-string, object> */
38+
/** @var array<class-string, object> */
3739
private array $reusedObjects = [];
3840

3941
/**
@@ -110,18 +112,23 @@ public function afterInstantiate(callable $callback): static
110112
* @psalm-return static<T>
111113
* @phpstan-return static
112114
*/
113-
final public function reuse(object $object): static
115+
final public function reuse(object ...$objects): static
114116
{
115-
if (isset($this->reusedObjects[$object::class])) {
116-
throw new \InvalidArgumentException(\sprintf('An object of class "%s" is already being reused.', $object::class));
117-
}
118-
119-
if ($object instanceof Factory) {
120-
throw new \InvalidArgumentException('Cannot reuse a factory.');
117+
if (0 === \count($objects)) {
118+
return $this;
121119
}
122120

123121
$clone = clone $this;
124-
$clone->reusedObjects[$object::class] = $object;
122+
123+
foreach ($objects as $object) {
124+
$object = unproxy($object, withAutoRefresh: false);
125+
126+
if ($object instanceof Factory) {
127+
throw new \InvalidArgumentException('Cannot reuse a factory.');
128+
}
129+
130+
$clone->reusedObjects[$object::class] = $object;
131+
}
125132

126133
return $clone;
127134
}
@@ -145,7 +152,7 @@ protected function normalizeParameter(string $field, mixed $value): mixed
145152
* @internal
146153
* @phpstan-return Parameters
147154
*/
148-
final protected function reusedAttributes(): array
155+
final protected function normalizeReusedAttributes(): array
149156
{
150157
if ([] === $this->reusedObjects) {
151158
return [];
@@ -184,4 +191,13 @@ final protected function reusedAttributes(): array
184191

185192
return $attributes;
186193
}
194+
195+
/**
196+
* @return list<object>
197+
* @internal
198+
*/
199+
final protected function reusedObjects(): array
200+
{
201+
return \array_values($this->reusedObjects);
202+
}
187203
}

src/Persistence/PersistentObjectFactory.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,9 @@ protected function normalizeParameter(string $field, mixed $value): mixed
301301
}
302302

303303
if ($value instanceof self) {
304-
$value = $value->withPersistMode($this->persist)->notRootFactory();
304+
$value = $value
305+
->withPersistMode($this->persist)
306+
->notRootFactory();
305307

306308
$pm = Configuration::instance()->persistence();
307309

@@ -311,9 +313,12 @@ protected function normalizeParameter(string $field, mixed $value): mixed
311313
if ($relationshipMetadata instanceof OneToOneRelationship && !$relationshipMetadata->isOwning) {
312314
$inverseField = $relationshipMetadata->inverseField();
313315

314-
$value = $value->withPersistMode(
315-
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
316-
);
316+
$value = $value
317+
->reuse(...$this->reusedObjects(), ...$value->reusedObjects())
318+
->withPersistMode(
319+
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
320+
)
321+
;
317322

318323
if (($fieldType = (new \ReflectionClass(static::class()))->getProperty($field)->getType())?->allowsNull()) {
319324
$this->tempAfterInstantiate[] = static function(object $object) use ($value, $inverseField, $field) {
@@ -363,9 +368,9 @@ protected function normalizeCollection(string $field, FactoryCollection $collect
363368
$this->tempAfterInstantiate[] = function(object $object) use ($collection, $inverseRelationshipMetadata, $field) {
364369
$inverseField = $inverseRelationshipMetadata->inverseField();
365370

366-
$inverseObjects = $collection->withPersistMode(
367-
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
368-
)
371+
$inverseObjects = $collection
372+
->reuse(...$this->reusedObjects())
373+
->withPersistMode($this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING)
369374
->create([$inverseField => $object]);
370375

371376
$inverseObjects = unproxy($inverseObjects, withAutoRefresh: false);
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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\Integration\ORM;
15+
16+
use PHPUnit\Framework\Attributes\Test;
17+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
18+
use Zenstruck\Foundry\Test\Factories;
19+
use Zenstruck\Foundry\Test\ResetDatabase;
20+
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\AddressFactory;
21+
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CategoryFactory;
22+
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ContactFactory;
23+
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag\TagFactory;
24+
use Zenstruck\Foundry\Tests\Integration\RequiresORM;
25+
26+
final class ReuseEntityTest extends KernelTestCase
27+
{
28+
use Factories, RequiresORM, ResetDatabase;
29+
30+
/**
31+
* @test
32+
*/
33+
#[Test]
34+
public function it_can_reuse_an_object_in_one_to_one(): void
35+
{
36+
$address = AddressFactory::createOne();
37+
38+
$contact = ContactFactory::new()
39+
->reuse($address)
40+
->create();
41+
42+
self::assertSame($address, $contact->getAddress());
43+
}
44+
45+
/**
46+
* @test
47+
*/
48+
#[Test]
49+
public function it_can_reuse_an_object_in_inverse_one_to_one(): void
50+
{
51+
$contact = ContactFactory::createOne();
52+
53+
$address = AddressFactory::new()
54+
->reuse($contact)
55+
->create();
56+
57+
self::assertSame($contact, $address->getContact());
58+
}
59+
60+
/**
61+
* @test
62+
*/
63+
#[Test]
64+
public function it_can_propagate_reused_objects_through_inversed_one_to_one(): void
65+
{
66+
$category = CategoryFactory::createOne();
67+
68+
$address = AddressFactory::new(['contact' => ContactFactory::new()])
69+
->reuse($category)
70+
->create();
71+
72+
self::assertSame($category, $address->getContact()?->getCategory());
73+
self::assertSame($category, $address->getContact()->getSecondaryCategory());
74+
}
75+
76+
/**
77+
* @test
78+
*/
79+
#[Test]
80+
public function reused_object_in_sub_factory_has_priority(): void
81+
{
82+
$category = CategoryFactory::createOne();
83+
84+
$address = AddressFactory::new([
85+
'contact' => ContactFactory::new()->reuse($category2 = CategoryFactory::createOne()),
86+
])
87+
->reuse($category)
88+
->create();
89+
90+
self::assertSame($category2, $address->getContact()?->getCategory());
91+
self::assertSame($category2, $address->getContact()->getSecondaryCategory());
92+
}
93+
94+
/**
95+
* @test
96+
*/
97+
#[Test]
98+
public function reuse_has_no_effect_on_collections(): void
99+
{
100+
$contact = ContactFactory::createOne();
101+
102+
$category = CategoryFactory::new()
103+
->reuse($contact)
104+
->create(['contacts' => ContactFactory::new()->many(2)]);
105+
106+
self::assertNotSame($contact, $category->getContacts()[0]);
107+
self::assertNotSame($contact, $category->getContacts()[1]);
108+
}
109+
110+
/**
111+
* @test
112+
*/
113+
#[Test]
114+
public function it_can_propagate_reused_objects_through_inversed_one_to_many(): void
115+
{
116+
$address = AddressFactory::createOne();
117+
118+
$category = CategoryFactory::new()
119+
->reuse($address)
120+
->create(['contacts' => ContactFactory::new()->many(2)]);
121+
122+
self::assertCount(2, $category->getContacts());
123+
foreach ($category->getContacts() as $contact) {
124+
self::assertSame($address, $contact->getAddress());
125+
}
126+
}
127+
128+
/**
129+
* @test
130+
*/
131+
#[Test]
132+
public function it_can_propagate_reused_objects_through_inversed_many_to_many(): void
133+
{
134+
$address = AddressFactory::createOne();
135+
136+
$tag = TagFactory::new()
137+
->reuse($address)
138+
->create(['contacts' => ContactFactory::new()->many(2)]);
139+
140+
self::assertCount(2, $tag->getContacts());
141+
foreach ($tag->getContacts() as $contact) {
142+
self::assertSame($address, $contact->getAddress());
143+
}
144+
}
145+
}

0 commit comments

Comments
 (0)