Skip to content

Commit 78009d9

Browse files
authored
Merge pull request #517 from gam6itko/load-eager-test
Add `unset` behavior
2 parents 0b65906 + 7e6708c commit 78009d9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1097
-619
lines changed

psalm-baseline.xml

Lines changed: 20 additions & 530 deletions
Large diffs are not rendered by default.

psalm.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
</ignoreFiles>
1414
</projectFiles>
1515
<issueHandlers>
16+
<MissingOverrideAttribute errorLevel="suppress" />
17+
<PossiblyUnusedMethod errorLevel="suppress" />
1618
<MissingClassConstType errorLevel="suppress" />
1719
<UnusedClass errorLevel="suppress" />
1820
<UndefinedAttributeClass>

src/Heap/Node.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Cycle\Database\Injection\ValueInterface;
88
use Cycle\ORM\Heap\Traits\RelationTrait;
99
use Cycle\ORM\Reference\ReferenceInterface;
10+
use Cycle\ORM\Relation\SpecialValue;
1011
use Cycle\ORM\RelationMap;
1112
use JetBrains\PhpStorm\ExpectedValues;
1213

@@ -191,6 +192,10 @@ public function syncState(RelationMap $relMap, State $state): array
191192
$changes = \array_udiff_assoc($state->getTransactionData(), $this->data, [self::class, 'compare']);
192193

193194
foreach ($state->getRelations() as $name => $value) {
195+
if (SpecialValue::isNotSet($value)) {
196+
continue;
197+
}
198+
194199
if ($value instanceof ReferenceInterface) {
195200
$changes[$name] = $value->hasValue() ? $value->getValue() : $value;
196201
}

src/ORM.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,19 @@ final class ORM implements ORMInterface
4646
private RepositoryProvider $repositoryProvider;
4747
private EntityProvider $entityProvider;
4848
private RoleResolverInterface $roleResolver;
49+
private Options $options;
4950

5051
public function __construct(
5152
private FactoryInterface $factory,
5253
private SchemaInterface $schema,
5354
?CommandGeneratorInterface $commandGenerator = null,
5455
?HeapInterface $heap = null,
56+
?Options $options = null,
5557
) {
5658
$this->heap = $heap ?? new Heap();
5759
$this->commandGenerator = $commandGenerator ?? new CommandGenerator();
5860
$this->resetRegistry();
61+
$this->options = $options ?? new Options();
5962
}
6063

6164
public function resolveRole(string|object $entity): string
@@ -93,6 +96,7 @@ public function getService(
9396
RepositoryProviderInterface::class,
9497
SourceProviderInterface::class,
9598
TypecastProviderInterface::class,
99+
Options::class,
96100
])]
97101
string $class,
98102
): object {
@@ -105,6 +109,7 @@ public function getService(
105109
MapperProviderInterface::class => $this->mapperProvider,
106110
RelationProviderInterface::class => $this->relationProvider,
107111
RepositoryProviderInterface::class => $this->repositoryProvider,
112+
Options::class => $this->options,
108113
default => throw new \InvalidArgumentException("Undefined service `$class`."),
109114
};
110115
}
@@ -161,6 +166,7 @@ public function with(
161166
?SchemaInterface $schema = null,
162167
?FactoryInterface $factory = null,
163168
?HeapInterface $heap = null,
169+
?Options $options = null,
164170
): ORMInterface {
165171
$heap ??= clone $this->heap;
166172
$heap->clean();
@@ -170,6 +176,7 @@ public function with(
170176
schema: $schema ?? $this->schema,
171177
commandGenerator: $this->commandGenerator,
172178
heap: $heap,
179+
options: $options,
173180
);
174181
}
175182

src/Options.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\ORM;
6+
7+
/**
8+
* ORM behavior options.
9+
*/
10+
final class Options
11+
{
12+
/**
13+
* @readonly
14+
*/
15+
public bool $ignoreUninitializedRelations = true;
16+
17+
/**
18+
* If TRUE, ORM will ignore relations on uninitialized Entity properties.
19+
* In this case, `unset($entity->relation)` will not change the relation when saving,
20+
* and it will hydrate it if the relation is loaded in the query.
21+
*
22+
* If FALSE, uninitialized properties will be treated as NULL (an empty collection or empty value).
23+
* `unset($entity->relation)` will lead to a change in the relation
24+
* (removing the link with another entity or entities).
25+
*/
26+
public function withIgnoreUninitializedRelations(bool $value): static
27+
{
28+
$clone = clone $this;
29+
$clone->ignoreUninitializedRelations = $value;
30+
return $clone;
31+
}
32+
}

src/Relation/BelongsTo.php

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,17 @@ public function __construct(ORMInterface $orm, string $role, string $name, strin
3636
public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = true): void
3737
{
3838
$state = $tuple->state;
39-
4039
$relName = $this->getName();
40+
41+
if (SpecialValue::isNotSet($related)) {
42+
if (!$state->hasRelation($relName)) {
43+
$state->setRelationStatus($relName, RelationInterface::STATUS_DEFERRED);
44+
return;
45+
}
46+
47+
$related = $state->getRelation($relName);
48+
}
49+
4150
if ($state->hasRelation($relName)) {
4251
$prefill = $state->getRelation($relName);
4352
$nodeValue = $tuple->node->getRelation($relName);
@@ -70,11 +79,18 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t
7079
public function queue(Pool $pool, Tuple $tuple): void
7180
{
7281
$state = $tuple->state;
73-
$related = $state->getRelation($this->getName());
82+
$relName = $this->getName();
83+
84+
if (!$state->hasRelation($relName)) {
85+
$state->setRelationStatus($relName, RelationInterface::STATUS_RESOLVED);
86+
return;
87+
}
88+
89+
$related = $state->getRelation($relName);
7490

7591
if ($related instanceof ReferenceInterface && $related->hasValue()) {
7692
$related = $related->getValue();
77-
$state->setRelation($this->getName(), $related);
93+
$state->setRelation($relName, $related);
7894
}
7995
if ($related === null) {
8096
$this->setNullFromRelated($tuple, false);
@@ -86,11 +102,11 @@ public function queue(Pool $pool, Tuple $tuple): void
86102
foreach ($this->outerKeys as $i => $outerKey) {
87103
$state->register($this->innerKeys[$i], $scope[$outerKey]);
88104
}
89-
$state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
105+
$state->setRelationStatus($relName, RelationInterface::STATUS_RESOLVED);
90106
return;
91107
}
92108
if ($tuple->status >= Tuple::STATUS_WAITED) {
93-
$state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
109+
$state->setRelationStatus($relName, RelationInterface::STATUS_RESOLVED);
94110
}
95111
return;
96112
}
@@ -99,8 +115,8 @@ public function queue(Pool $pool, Tuple $tuple): void
99115

100116
if ($this->shouldPull($tuple, $rTuple)) {
101117
$this->pullValues($state, $rTuple->state);
102-
$state->setRelation($this->getName(), $related);
103-
$state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
118+
$state->setRelation($relName, $related);
119+
$state->setRelationStatus($relName, RelationInterface::STATUS_RESOLVED);
104120
}
105121
}
106122

src/Relation/Embedded.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ public function queue(Pool $pool, Tuple $tuple, ?StoreCommandInterface $command
120120
if ($tuple->task !== Tuple::TASK_STORE) {
121121
return;
122122
}
123+
124+
if (!$tuple->state->hasRelation($this->getName())) {
125+
$tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
126+
return;
127+
}
128+
123129
$related = $tuple->state->getRelation($this->getName());
124130

125131
// Master Node

src/Relation/HasMany.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,17 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t
5656
$tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
5757
return;
5858
}
59+
5960
$original = $this->resolve($original, true);
6061
$node->setRelation($this->getName(), $original);
6162
}
6263

6364
if ($related instanceof ReferenceInterface) {
6465
$related = $this->resolve($related, true);
6566
$tuple->state->setRelation($this->getName(), $related);
67+
} elseif (SpecialValue::isNotSet($related)) {
68+
$tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
69+
return;
6670
} elseif (!\is_iterable($related)) {
6771
if ($related === null) {
6872
$related = $this->collect([]);
@@ -73,8 +77,10 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t
7377
));
7478
}
7579
}
76-
foreach ($this->calcDeleted($related, $original ?? []) as $item) {
77-
$this->deleteChild($pool, $tuple, $item);
80+
if (!SpecialValue::isEmpty($original)) {
81+
foreach ($this->calcDeleted($related, $original) as $item) {
82+
$this->deleteChild($pool, $tuple, $item);
83+
}
7884
}
7985

8086
if (\count($related) === 0) {

src/Relation/HasOne.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t
5353
if ($related instanceof ReferenceInterface) {
5454
$related = $this->resolve($related, true);
5555
$tuple->state->setRelation($this->getName(), $related);
56+
} elseif (SpecialValue::isNotSet($related)) {
57+
$tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
58+
return;
5659
}
5760

5861
if ($related === null) {

src/Relation/ManyToMany.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t
8484
if ($related instanceof ReferenceInterface && $this->resolve($related, true) !== null) {
8585
$related = $related->getValue();
8686
$tuple->state->setRelation($this->getName(), $related);
87+
} elseif (SpecialValue::isNotSet($related)) {
88+
$tuple->state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED);
89+
return;
8790
}
91+
8892
$related = $this->extractRelated($related, $original);
8993
// $tuple->state->setStorage($this->pivotEntity, $related);
9094
$tuple->state->setRelation($this->getName(), $related);

0 commit comments

Comments
 (0)