Completed
Push — 2.x ( d712c7...0313e8 )
by Aleksei
13s queued 10s
created

UnitOfWork::resolveSelfWithEmbedded()   C

Complexity

Conditions 14
Paths 42

Size

Total Lines 47
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 14.0713

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 14
eloc 29
c 2
b 0
f 0
nc 42
nop 3
dl 0
loc 47
ccs 26
cts 28
cp 0.9286
crap 14.0713
rs 6.2666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\ORM\Transaction;
6
7
use Cycle\ORM\Command\CommandInterface;
8
use Cycle\ORM\Command\Special\Sequence;
9
use Cycle\ORM\Exception\PoolException;
10
use Cycle\ORM\Exception\SuccessTransactionRetryException;
11
use Cycle\ORM\Exception\TransactionException;
12
use Cycle\ORM\Heap\Node;
13
use Cycle\ORM\Options;
14
use Cycle\ORM\ORMInterface;
15
use Cycle\ORM\Relation\SpecialValue;
16
use Cycle\ORM\Service\IndexProviderInterface;
17
use Cycle\ORM\Service\RelationProviderInterface;
18
use Cycle\ORM\Relation\RelationInterface;
19
use Cycle\ORM\Relation\ShadowBelongsTo;
20
use Cycle\ORM\RelationMap;
21
22
final class UnitOfWork implements StateInterface
23
{
24
    private const RELATIONS_NOT_RESOLVED = 0;
25
    private const RELATIONS_RESOLVED = 1;
26
    private const RELATIONS_DEFERRED = 2;
27
    private const STAGE_PREPARING = 0;
28
    private const STAGE_PROCESS = 1;
29
    private const STAGE_FINISHED = 2;
30
31
    private int $stage = self::STAGE_PREPARING;
32
    private Pool $pool;
33
    private CommandGeneratorInterface $commandGenerator;
34
    private ?\Throwable $error = null;
35
    private bool $ignoreUninitializedRelations;
36
37
    public function __construct(
38 2916
        private ORMInterface $orm,
39
        private ?RunnerInterface $runner = null,
40
    ) {
41
        $this->pool = new Pool($orm);
42 2916
        $this->commandGenerator = $orm->getCommandGenerator();
43 2916
        $this->ignoreUninitializedRelations = $orm->getService(Options::class)->ignoreUninitializedRelations;
44
    }
45
46 2916
    public function isSuccess(): bool
47
    {
48 2916
        return $this->stage === self::STAGE_FINISHED;
49
    }
50
51 176
    public function getLastError(): ?\Throwable
52
    {
53 176
        return $this->error;
54
    }
55
56 16
    public function retry(): static
57
    {
58 16
        return $this->run();
59
    }
60
61 80
    public function persistState(object $entity, bool $cascade = true): self
62
    {
63 80
        $this->checkActionPossibility('persist entity');
64 80
        $this->pool->attachStore($entity, $cascade, persist: true);
65
66 80
        return $this;
67
    }
68
69 2764
    public function persistDeferred(object $entity, bool $cascade = true): self
70
    {
71 2764
        $this->checkActionPossibility('schedule entity storing');
72 2764
        $this->pool->attachStore($entity, $cascade);
73
74 2764
        return $this;
75
    }
76
77 184
    public function delete(object $entity, bool $cascade = true): self
78
    {
79 184
        $this->checkActionPossibility('schedule entity deletion');
80 184
        $this->pool->attach($entity, Tuple::TASK_FORCE_DELETE, $cascade);
81
82 184
        return $this;
83
    }
84
85 2916
    public function run(): StateInterface
86
    {
87 2916
        $this->stage = match ($this->stage) {
88 1462
            self::STAGE_FINISHED => throw new SuccessTransactionRetryException(
89
                'A successful transaction cannot be re-run.',
90
            ),
91 1458
            self::STAGE_PROCESS => throw new TransactionException('Can\'t run started transaction.'),
92 2916
            default => self::STAGE_PROCESS,
93
        };
94
95 2916
        $this->runner ??= Runner::innerTransaction();
96
97
        try {
98
            try {
99 2916
                $this->walkPool();
100 168
            } catch (PoolException $e) {
101
                // Generate detailed exception about unresolved relations
102 24
                throw TransactionException::unresolvedRelations(
103 24
                    $this->pool->getUnresolved(),
104 2828
                    $this->orm->getService(RelationProviderInterface::class),
105
                    $e,
106
                );
107 168
            }
108 168
        } catch (\Throwable $e) {
109 168
            $this->runner->rollback();
0 ignored issues
show
Bug introduced by
The method rollback() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

109
            $this->runner->/** @scrutinizer ignore-call */ 
110
                           rollback();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
110
            $this->pool->closeIterator();
111
112
            // no calculations must be kept in node states, resetting
113 168
            // this will keep entity data as it was before transaction run
114
            $this->resetHeap();
115 168
116 168
            $this->error = $e;
117
            $this->stage = self::STAGE_PREPARING;
118 168
119
            return $this;
120
        }
121
122 2804
        // we are ready to commit all changes to our representation layer
123
        $this->syncHeap();
124 2804
125 2804
        $this->runner->complete();
126 2804
        $this->error = null;
127
        $this->stage = self::STAGE_FINISHED;
128 2804
        // Clear state
129
        unset($this->orm, $this->runner, $this->pool, $this->commandGenerator);
130 2804
131
        return $this;
132
    }
133
134
    public function setRunner(RunnerInterface $runner): void
135
    {
136
        $this->runner = $runner;
137
    }
138
139
    /**
140
     * @throws TransactionException
141 2916
     */
142
    private function checkActionPossibility(string $action): void
143 2916
    {
144 2916
        $this->stage === self::STAGE_PROCESS && throw new TransactionException("Can't $action. Transaction began.");
145
        $this->stage === self::STAGE_FINISHED && throw new TransactionException("Can't $action. Transaction finished.");
146
    }
147 2588
148
    private function runCommand(?CommandInterface $command): void
149 2588
    {
150
        if ($command === null) {
151
            return;
152 2588
        }
153 2564
        $this->runner->run($command);
154
        $this->pool->someHappens();
155
    }
156
157
    /**
158
     * Sync all entity states with generated changes.
159 2804
     */
160
    private function syncHeap(): void
161 2804
    {
162 2804
        $heap = $this->orm->getHeap();
163 2804
        $relationProvider = $this->orm->getService(RelationProviderInterface::class);
164 2804
        $indexProvider = $this->orm->getService(IndexProviderInterface::class);
165 2788
        foreach ($this->pool->getAllTuples() as $e => $tuple) {
166
            $node = $tuple->node;
167
168 2788
            // marked as being deleted and has no external claims (GC like approach)
169 464
            if (\in_array($node->getStatus(), [Node::DELETED, Node::SCHEDULED_DELETE], true)) {
170 464
                $heap->detach($e);
171
                continue;
172 2684
            }
173
            $role = $node->getRole();
174
175 2684
            // reindex the entity while it has old data
176 2684
            $node->setState($tuple->state);
177
            $heap->attach($e, $node, $indexProvider->getIndexes($role));
178 2684
179 40
            if ($tuple->persist !== null) {
180 40
                $syncData = \array_udiff_assoc(
181 40
                    $tuple->state->getData(),
182
                    $tuple->persist->getData(),
183
                    [Node::class, 'compare'],
184
                );
185 2660
            } else {
186
                $syncData = $node->syncState($relationProvider->getRelationMap($role), $tuple->state);
187
            }
188 2684
189
            $tuple->mapper->hydrate($e, $syncData);
190
        }
191
    }
192
193
    /**
194
     * Reset heap to it's initial state and remove all the changes.
195 168
     */
196
    private function resetHeap(): void
197 168
    {
198 168
        foreach ($this->pool->getAllTuples() as $tuple) {
199
            $tuple->node->resetState();
200
        }
201
    }
202
203
    /**
204
     * Return flattened list of commands required to store and delete associated entities.
205 2916
     */
206
    private function walkPool(): void
207
    {
208
        /**
209
         * @var object $entity
210
         * @var Tuple $tuple
211 2916
         */
212 2900
        foreach ($this->pool->openIterator() as $tuple) {
213
            if ($tuple->task === Tuple::TASK_FORCE_DELETE && !$tuple->cascade) {
214
                // currently we rely on db to delete all nested records (or soft deletes)
215
                $command = $this->commandGenerator->generateDeleteCommand($this->orm, $tuple);
216
                $this->runCommand($command);
217
                $tuple->status = Tuple::STATUS_PROCESSED;
218
                continue;
219
            }
220
221 2900
            // Walk relations
222
            $this->resolveRelations($tuple);
223
        }
224
    }
225 2900
226
    private function resolveMasterRelations(Tuple $tuple, RelationMap $map): int
227 2900
    {
228 2374
        if (!$map->hasDependencies()) {
229
            return self::RELATIONS_RESOLVED;
230
        }
231 2080
232 2080
        $deferred = false;
233 2080
        $resolved = true;
234 2080
        foreach ($map->getMasters() as $name => $relation) {
235 2080
            $statusAfter = $statusBefore = $tuple->state->getRelationStatus($relation->getName());
236 1230
            if ($statusBefore === RelationInterface::STATUS_RESOLVED) {
237
                continue;
238
            }
239 1728
240
            if ($relation instanceof ShadowBelongsTo) {
241
                // Check relation is connected
242
                // Connected -> $parentNode->getRelationStatus()
243 1264
                // Disconnected -> WAIT if Tuple::STATUS_PREPARING
244 1264
                $relation->queue($this->pool, $tuple);
245
                $statusAfter = $tuple->state->getRelationStatus($relation->getName());
246 920
            } else {
247 864
                if ($tuple->status === Tuple::STATUS_PREPARING) {
248 864
                    if ($statusAfter === RelationInterface::STATUS_PREPARE) {
249 864
                        $entityData ??= $tuple->mapper->fetchRelations($tuple->entity);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $entityData does not seem to be defined for all execution paths leading up to this point.
Loading history...
250 864
                        $relation->prepare(
251
                            $this->pool,
252
                            $tuple,
253 888
                            \array_key_exists($name, $entityData)
254 888
                                ? $entityData[$name]
255
                                : ($this->ignoreUninitializedRelations ? SpecialValue::notSet() : null),
256
                        );
257 1728
                        $statusAfter = $tuple->state->getRelationStatus($relation->getName());
258 1728
                    }
259
                } else {
260
                    $relation->queue($this->pool, $tuple);
261
                    $statusAfter = $tuple->state->getRelationStatus($relation->getName());
262 2080
                }
263
            }
264
265 2860
            $statusAfter > $statusBefore and $this->pool->someHappens();
266
            $resolved = $resolved && $statusAfter >= RelationInterface::STATUS_DEFERRED;
267 2860
            $deferred = $deferred || $statusAfter === RelationInterface::STATUS_DEFERRED;
268 2206
        }
269
270 1718
        // $tuple->waitKeys = array_unique(array_merge(...$waitKeys));
271
        return ($deferred ? self::RELATIONS_DEFERRED : 0) | ($resolved ? self::RELATIONS_RESOLVED : 0);
272
    }
273 1718
274 1718
    private function resolveSlaveRelations(Tuple $tuple, RelationMap $map): int
275 1718
    {
276 1718
        if (!$map->hasSlaves()) {
277 552
            return self::RELATIONS_RESOLVED;
278
        }
279 1718
        $changedFields = \array_keys($tuple->state->getChanges());
280 1718
281 1718
        // Attach children to pool
282 716
        $transactData = $tuple->state->getTransactionData();
283
        $deferred = false;
284
        $resolved = true;
285 1718
        if ($tuple->status === Tuple::STATUS_PREPARING) {
286 1718
            $relData = $tuple->mapper->fetchRelations($tuple->entity);
287 1718
        }
288 1718
        foreach ($map->getSlaves() as $name => $relation) {
289 1718
            $statusBefore = $statusAfter = $tuple->state->getRelationStatus($relation->getName());
290 1718
            if (!$relation->isCascade() || $statusAfter === RelationInterface::STATUS_RESOLVED) {
291 1718
                continue;
292
            }
293 1718
294 1718
            $innerKeys = $relation->getInnerKeys();
295
            $isWaitingKeys = \array_intersect($innerKeys, $tuple->state->getWaitingFields(true)) !== [];
296 1718
            $hasChangedKeys = \array_intersect($innerKeys, $changedFields) !== [];
297
            if ($statusAfter === RelationInterface::STATUS_PREPARE) {
298
                $relData ??= $tuple->mapper->fetchRelations($tuple->entity);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $relData does not seem to be defined for all execution paths leading up to this point.
Loading history...
299 1718
                $relation->prepare(
300 1718
                    $this->pool,
301
                    $tuple,
302
                    \array_key_exists($name, $relData)
303 1718
                        ? $relData[$name]
304
                        : ($this->ignoreUninitializedRelations ? SpecialValue::notSet() : null),
305 1322
                    $isWaitingKeys || $hasChangedKeys,
306 1322
                );
307 1322
                $statusAfter = $tuple->state->getRelationStatus($relation->getName());
308
            }
309 1718
310 1718
            if ($statusAfter !== RelationInterface::STATUS_PREPARE
311
                && $statusAfter !== RelationInterface::STATUS_RESOLVED
312
                && !$isWaitingKeys
313 1718
                && !$hasChangedKeys
314
                && \count(\array_intersect($innerKeys, \array_keys($transactData))) === \count($innerKeys)
315
            ) {
316 2764
                // $child ??= $tuple->state->getRelation($name);
317
                $relation->queue($this->pool, $tuple);
318 2764
                $statusAfter = $tuple->state->getRelationStatus($relation->getName());
319 1916
            }
320 1892
321 88
            $statusAfter > $statusBefore and $this->pool->someHappens();
322
            $resolved = $resolved && $statusAfter === RelationInterface::STATUS_RESOLVED;
323 1916
            $deferred = $deferred || $statusAfter === RelationInterface::STATUS_DEFERRED;
324
        }
325 2396
326
        return ($deferred ? self::RELATIONS_DEFERRED : 0) | ($resolved ? self::RELATIONS_RESOLVED : 0);
327 2396
    }
328
329 2228
    private function resolveSelfWithEmbedded(Tuple $tuple, RelationMap $map, bool $hasDeferredRelations): void
330
    {
331 2204
        $hasChanges = $tuple->state->hasChanges();
332 368
        if (!$hasChanges && !$map->hasEmbedded()) {
333 2084
            $tuple->status = $hasDeferredRelations
334
                ? \max(Tuple::STATUS_DEFERRED_RESOLVED, $tuple->status)
335 2204
                : Tuple::STATUS_PROCESSED;
336
337
            return;
338 168
        }
339 168
        $command = $this->commandGenerator->generateStoreCommand($this->orm, $tuple);
340 168
341 168
        if (!$map->hasEmbedded()) {
342
            // Not embedded but has changes
343
            $this->runCommand($command);
344 168
345
            $tuple->status = $tuple->status <= Tuple::STATUS_PROPOSED_RESOLVED && $hasDeferredRelations
346 168
                ? Tuple::STATUS_DEFERRED_RESOLVED
347 168
                : Tuple::STATUS_PROCESSED;
348
349 168
            return;
350
        }
351
352 152
        $entityData = $tuple->mapper->extract($tuple->entity);
353
        $chEmb = false;
354 152
        foreach ($map->getEmbedded() as $name => $relation) {
355 152
            $relationStatus = $tuple->state->getRelationStatus($relation->getName());
356
            if ($relationStatus === RelationInterface::STATUS_RESOLVED) {
357
                continue;
358
            }
359 2900
360
            $chEmb = true;
361 2900
            $tuple->state->setRelation($name, $entityData[$name] ?? null);
362
            // We can use class MergeCommand here
363
            $relation->queue(
364 2900
                $this->pool,
365 2796
                $tuple,
366 496
                $command instanceof Sequence ? $command->getPrimaryCommand() : $command,
367 2900
            );
368 2900
        }
369
370
        // Run command if there are embedded changes or other changes
371 2900
        $chEmb || $hasChanges and $this->runCommand($command);
372 486
373
        $tuple->status = $tuple->status === Tuple::STATUS_PREPROCESSED || !$hasDeferredRelations
374
            ? Tuple::STATUS_PROCESSED
375 2900
            : \max(Tuple::STATUS_DEFERRED_RESOLVED, $tuple->status);
376 2868
    }
377 2764
378 496
    private function resolveRelations(Tuple $tuple): void
379 496
    {
380
        $map = $this->orm->getRelationMap($tuple->node->getRole());
381 496
382 496
        $result = $tuple->task === Tuple::TASK_STORE
383 480
            ? $this->resolveMasterRelations($tuple, $map)
384
            : $this->resolveSlaveRelations($tuple, $map);
385
        $isDependenciesResolved = (bool) ($result & self::RELATIONS_RESOLVED);
386
        $deferred = (bool) ($result & self::RELATIONS_DEFERRED);
387 2860
388
        // If deferred relations found, mark self as deferred
389 2860
        if ($deferred) {
390 2756
            $tuple->status === Tuple::STATUS_PROPOSED_RESOLVED or $this->pool->someHappens();
391 496
            $tuple->status = $isDependenciesResolved ? Tuple::STATUS_DEFERRED_RESOLVED : Tuple::STATUS_DEFERRED;
392
        }
393
394 2860
        if ($isDependenciesResolved) {
395 24
            if ($tuple->task === Tuple::TASK_STORE) {
396
                $this->resolveSelfWithEmbedded($tuple, $map, $deferred);
397
            } elseif ($tuple->status === Tuple::STATUS_PREPARING) {
398
                $tuple->status = Tuple::STATUS_WAITING;
399
            } else {
400
                $command = $this->commandGenerator->generateDeleteCommand($this->orm, $tuple);
401
                $this->runCommand($command);
402
                $tuple->status = Tuple::STATUS_PROCESSED;
403
            }
404
        }
405
406
        if ($tuple->cascade) {
407
            $tuple->task === Tuple::TASK_STORE
408
                ? $this->resolveSlaveRelations($tuple, $map)
409
                : $this->resolveMasterRelations($tuple, $map);
410
        }
411
412
        if (!$isDependenciesResolved && $tuple->status === Tuple::STATUS_PREPROCESSED) {
413
            $tuple->status = Tuple::STATUS_UNPROCESSED;
414
        }
415
    }
416
}
417