Skip to content

Commit 0313e88

Browse files
authored
Merge pull request #534: Fix RefersTo relation
2 parents d712c79 + c95a47e commit 0313e88

File tree

19 files changed

+606
-37
lines changed

19 files changed

+606
-37
lines changed

phpunit.xml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@
1010
convertErrorsToExceptions="true"
1111
convertNoticesToExceptions="true"
1212
convertWarningsToExceptions="true"
13-
convertDeprecationsToExceptions="true"
13+
convertDeprecationsToExceptions="false"
1414
processIsolation="false"
15-
stopOnFailure="true"
16-
stopOnError="true"
1715
>
1816
<coverage>
1917
<include>

src/Relation/BelongsTo.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public function queue(Pool $pool, Tuple $tuple): void
122122

123123
private function shouldPull(Tuple $tuple, Tuple $rTuple): bool
124124
{
125-
$minStatus = Tuple::STATUS_PREPROCESSED;
125+
$minStatus = Tuple::STATUS_DEFERRED_RESOLVED;
126126
if ($this->inversion !== null) {
127127
$relName = $this->getTargetRelationName();
128128
if ($rTuple->state->getRelationStatus($relName) === RelationInterface::STATUS_RESOLVED) {

src/Relation/RefersTo.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t
5555
return;
5656
}
5757
$this->registerWaitingFields($tuple->state, false);
58+
5859
if ($related instanceof ReferenceInterface) {
5960
$tuple->state->setRelationStatus($relName, RelationInterface::STATUS_DEFERRED);
6061
return;
@@ -98,14 +99,15 @@ public function queue(Pool $pool, Tuple $tuple): void
9899
if ($this->checkNullValue($tuple->node, $tuple->state, $related)) {
99100
return;
100101
}
102+
101103
$rTuple = $pool->offsetGet($related);
102104
if ($rTuple === null) {
103105
if ($this->isCascade()) {
104106
// todo: cascade true?
105107
$rTuple = $pool->attachStore($related, false, null, null, false);
106108
} elseif (
107109
$tuple->state->getRelationStatus($relName) !== RelationInterface::STATUS_DEFERRED
108-
|| $tuple->status !== Tuple::STATUS_PROPOSED
110+
|| $tuple->status !== Tuple::STATUS_PROPOSED_RESOLVED
109111
) {
110112
$tuple->state->setRelationStatus($relName, RelationInterface::STATUS_DEFERRED);
111113
return;

src/Transaction/Pool.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ public function openIterator(): \Traversable
191191
$tuple->status = Tuple::STATUS_WAITED;
192192
} elseif ($tuple->status === Tuple::STATUS_DEFERRED) {
193193
$tuple->status = Tuple::STATUS_PROPOSED;
194+
} elseif ($tuple->status === Tuple::STATUS_DEFERRED_RESOLVED) {
195+
$tuple->status = Tuple::STATUS_PROPOSED_RESOLVED;
194196
}
195197
yield $entity => $tuple;
196198
$this->trashIt($entity, $tuple, $this->storage);
@@ -208,6 +210,7 @@ public function openIterator(): \Traversable
208210
$this->unprocessed = [];
209211
continue;
210212
}
213+
211214
if ($this->happens === 0 && (\count($pool) > 0 || $hasUnresolved)) {
212215
throw new PoolException('Pool has gone into an infinite loop.');
213216
}

src/Transaction/Tuple.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ final class Tuple
2222
public const STATUS_WAITED = 2;
2323
public const STATUS_DEFERRED = 3;
2424
public const STATUS_PROPOSED = 4;
25-
public const STATUS_PREPROCESSED = 5;
26-
public const STATUS_PROCESSED = 6;
27-
public const STATUS_UNPROCESSED = 7;
25+
public const STATUS_DEFERRED_RESOLVED = 5;
26+
public const STATUS_PROPOSED_RESOLVED = 6;
27+
public const STATUS_PREPROCESSED = 7;
28+
public const STATUS_PROCESSED = 8;
29+
public const STATUS_UNPROCESSED = 9;
2830

2931
public Node $node;
3032
public State $state;
@@ -46,6 +48,8 @@ public function __construct(
4648
self::STATUS_WAITED,
4749
self::STATUS_DEFERRED,
4850
self::STATUS_PROPOSED,
51+
self::STATUS_DEFERRED_RESOLVED,
52+
self::STATUS_PROPOSED_RESOLVED,
4953
self::STATUS_PREPROCESSED,
5054
self::STATUS_PROCESSED,
5155
self::STATUS_UNPROCESSED,

src/Transaction/UnitOfWork.php

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,8 @@ private function resolveMasterRelations(Tuple $tuple, RelationMap $map): int
232232
$deferred = false;
233233
$resolved = true;
234234
foreach ($map->getMasters() as $name => $relation) {
235-
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
236-
if ($relationStatus === RelationInterface::STATUS_RESOLVED) {
235+
$statusAfter = $statusBefore = $tuple->state->getRelationStatus($relation->getName());
236+
if ($statusBefore === RelationInterface::STATUS_RESOLVED) {
237237
continue;
238238
}
239239

@@ -242,10 +242,10 @@ private function resolveMasterRelations(Tuple $tuple, RelationMap $map): int
242242
// Connected -> $parentNode->getRelationStatus()
243243
// Disconnected -> WAIT if Tuple::STATUS_PREPARING
244244
$relation->queue($this->pool, $tuple);
245-
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
245+
$statusAfter = $tuple->state->getRelationStatus($relation->getName());
246246
} else {
247247
if ($tuple->status === Tuple::STATUS_PREPARING) {
248-
if ($relationStatus === RelationInterface::STATUS_PREPARE) {
248+
if ($statusAfter === RelationInterface::STATUS_PREPARE) {
249249
$entityData ??= $tuple->mapper->fetchRelations($tuple->entity);
250250
$relation->prepare(
251251
$this->pool,
@@ -254,15 +254,17 @@ private function resolveMasterRelations(Tuple $tuple, RelationMap $map): int
254254
? $entityData[$name]
255255
: ($this->ignoreUninitializedRelations ? SpecialValue::notSet() : null),
256256
);
257-
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
257+
$statusAfter = $tuple->state->getRelationStatus($relation->getName());
258258
}
259259
} else {
260260
$relation->queue($this->pool, $tuple);
261-
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
261+
$statusAfter = $tuple->state->getRelationStatus($relation->getName());
262262
}
263263
}
264-
$resolved = $resolved && $relationStatus >= RelationInterface::STATUS_DEFERRED;
265-
$deferred = $deferred || $relationStatus === RelationInterface::STATUS_DEFERRED;
264+
265+
$statusAfter > $statusBefore and $this->pool->someHappens();
266+
$resolved = $resolved && $statusAfter >= RelationInterface::STATUS_DEFERRED;
267+
$deferred = $deferred || $statusAfter === RelationInterface::STATUS_DEFERRED;
266268
}
267269

268270
// $tuple->waitKeys = array_unique(array_merge(...$waitKeys));
@@ -284,15 +286,15 @@ private function resolveSlaveRelations(Tuple $tuple, RelationMap $map): int
284286
$relData = $tuple->mapper->fetchRelations($tuple->entity);
285287
}
286288
foreach ($map->getSlaves() as $name => $relation) {
287-
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
288-
if (!$relation->isCascade() || $relationStatus === RelationInterface::STATUS_RESOLVED) {
289+
$statusBefore = $statusAfter = $tuple->state->getRelationStatus($relation->getName());
290+
if (!$relation->isCascade() || $statusAfter === RelationInterface::STATUS_RESOLVED) {
289291
continue;
290292
}
291293

292294
$innerKeys = $relation->getInnerKeys();
293295
$isWaitingKeys = \array_intersect($innerKeys, $tuple->state->getWaitingFields(true)) !== [];
294296
$hasChangedKeys = \array_intersect($innerKeys, $changedFields) !== [];
295-
if ($relationStatus === RelationInterface::STATUS_PREPARE) {
297+
if ($statusAfter === RelationInterface::STATUS_PREPARE) {
296298
$relData ??= $tuple->mapper->fetchRelations($tuple->entity);
297299
$relation->prepare(
298300
$this->pool,
@@ -302,32 +304,35 @@ private function resolveSlaveRelations(Tuple $tuple, RelationMap $map): int
302304
: ($this->ignoreUninitializedRelations ? SpecialValue::notSet() : null),
303305
$isWaitingKeys || $hasChangedKeys,
304306
);
305-
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
307+
$statusAfter = $tuple->state->getRelationStatus($relation->getName());
306308
}
307309

308-
if ($relationStatus !== RelationInterface::STATUS_PREPARE
309-
&& $relationStatus !== RelationInterface::STATUS_RESOLVED
310+
if ($statusAfter !== RelationInterface::STATUS_PREPARE
311+
&& $statusAfter !== RelationInterface::STATUS_RESOLVED
310312
&& !$isWaitingKeys
311313
&& !$hasChangedKeys
312314
&& \count(\array_intersect($innerKeys, \array_keys($transactData))) === \count($innerKeys)
313315
) {
314316
// $child ??= $tuple->state->getRelation($name);
315317
$relation->queue($this->pool, $tuple);
316-
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
318+
$statusAfter = $tuple->state->getRelationStatus($relation->getName());
317319
}
318-
$resolved = $resolved && $relationStatus === RelationInterface::STATUS_RESOLVED;
319-
$deferred = $deferred || $relationStatus === RelationInterface::STATUS_DEFERRED;
320+
321+
$statusAfter > $statusBefore and $this->pool->someHappens();
322+
$resolved = $resolved && $statusAfter === RelationInterface::STATUS_RESOLVED;
323+
$deferred = $deferred || $statusAfter === RelationInterface::STATUS_DEFERRED;
320324
}
321325

322326
return ($deferred ? self::RELATIONS_DEFERRED : 0) | ($resolved ? self::RELATIONS_RESOLVED : 0);
323327
}
324328

325329
private function resolveSelfWithEmbedded(Tuple $tuple, RelationMap $map, bool $hasDeferredRelations): void
326330
{
327-
if (!$map->hasEmbedded() && !$tuple->state->hasChanges()) {
328-
$tuple->status = !$hasDeferredRelations
329-
? Tuple::STATUS_PROCESSED
330-
: \max(Tuple::STATUS_DEFERRED, $tuple->status);
331+
$hasChanges = $tuple->state->hasChanges();
332+
if (!$hasChanges && !$map->hasEmbedded()) {
333+
$tuple->status = $hasDeferredRelations
334+
? \max(Tuple::STATUS_DEFERRED_RESOLVED, $tuple->status)
335+
: Tuple::STATUS_PROCESSED;
331336

332337
return;
333338
}
@@ -337,19 +342,22 @@ private function resolveSelfWithEmbedded(Tuple $tuple, RelationMap $map, bool $h
337342
// Not embedded but has changes
338343
$this->runCommand($command);
339344

340-
$tuple->status = $tuple->status <= Tuple::STATUS_PROPOSED && $hasDeferredRelations
341-
? Tuple::STATUS_DEFERRED
345+
$tuple->status = $tuple->status <= Tuple::STATUS_PROPOSED_RESOLVED && $hasDeferredRelations
346+
? Tuple::STATUS_DEFERRED_RESOLVED
342347
: Tuple::STATUS_PROCESSED;
343348

344349
return;
345350
}
346351

347352
$entityData = $tuple->mapper->extract($tuple->entity);
353+
$chEmb = false;
348354
foreach ($map->getEmbedded() as $name => $relation) {
349355
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
350356
if ($relationStatus === RelationInterface::STATUS_RESOLVED) {
351357
continue;
352358
}
359+
360+
$chEmb = true;
353361
$tuple->state->setRelation($name, $entityData[$name] ?? null);
354362
// We can use class MergeCommand here
355363
$relation->queue(
@@ -358,11 +366,13 @@ private function resolveSelfWithEmbedded(Tuple $tuple, RelationMap $map, bool $h
358366
$command instanceof Sequence ? $command->getPrimaryCommand() : $command,
359367
);
360368
}
361-
$this->runCommand($command);
369+
370+
// Run command if there are embedded changes or other changes
371+
$chEmb || $hasChanges and $this->runCommand($command);
362372

363373
$tuple->status = $tuple->status === Tuple::STATUS_PREPROCESSED || !$hasDeferredRelations
364374
? Tuple::STATUS_PROCESSED
365-
: \max(Tuple::STATUS_DEFERRED, $tuple->status);
375+
: \max(Tuple::STATUS_DEFERRED_RESOLVED, $tuple->status);
366376
}
367377

368378
private function resolveRelations(Tuple $tuple): void
@@ -375,9 +385,10 @@ private function resolveRelations(Tuple $tuple): void
375385
$isDependenciesResolved = (bool) ($result & self::RELATIONS_RESOLVED);
376386
$deferred = (bool) ($result & self::RELATIONS_DEFERRED);
377387

378-
// Self
379-
if ($deferred && $tuple->status < Tuple::STATUS_PROPOSED) {
380-
$tuple->status = Tuple::STATUS_DEFERRED;
388+
// If deferred relations found, mark self as deferred
389+
if ($deferred) {
390+
$tuple->status === Tuple::STATUS_PROPOSED_RESOLVED or $this->pool->someHappens();
391+
$tuple->status = $isDependenciesResolved ? Tuple::STATUS_DEFERRED_RESOLVED : Tuple::STATUS_DEFERRED;
381392
}
382393

383394
if ($isDependenciesResolved) {

tests/ORM/Functional/Driver/Common/Classless/ClasslessHasOneCyclicTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ public function testCreateCyclic(): void
9898
$this->save($c);
9999
$this->assertNumWrites(2);
100100

101+
$this->captureWriteQueries();
102+
$this->save($c);
103+
$this->assertNumWrites(0);
104+
101105
$selector = new Select($this->orm->withHeap(new Heap()), 'cyclic');
102106
$c = $selector->load('cyclic')->wherePK($c->id)->fetchOne();
103107
$this->assertEquals('new', $c->name);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity;
6+
7+
class Category
8+
{
9+
public ?int $id = null;
10+
public string $name;
11+
public \DateTimeImmutable $created_at;
12+
public \DateTimeImmutable $updated_at;
13+
14+
public function __construct(string $name)
15+
{
16+
$this->name = $name;
17+
$this->created_at = new \DateTimeImmutable();
18+
$this->updated_at = new \DateTimeImmutable();
19+
}
20+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity;
6+
7+
class Comment
8+
{
9+
public \DateTimeImmutable $created_at;
10+
public \DateTimeImmutable $updated_at;
11+
public ?int $post_id = null;
12+
public ?int $user_id = null;
13+
public ?Comment $parent = null;
14+
public ?int $parent_id = null;
15+
16+
public function __construct(
17+
public int $id,
18+
public string $content,
19+
public Post $post,
20+
public User $user,
21+
) {
22+
$this->created_at = new \DateTimeImmutable();
23+
$this->updated_at = new \DateTimeImmutable();
24+
}
25+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity;
6+
7+
class Metadata
8+
{
9+
public function __construct(
10+
public string $data = '',
11+
) {}
12+
}

0 commit comments

Comments
 (0)