UnitOfWork::resolveSelfWithEmbedded()   C
last analyzed

Complexity

Conditions 12
Paths 18

Size

Total Lines 41
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 12.0832

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 26
c 2
b 0
f 0
nc 18
nop 3
dl 0
loc 41
ccs 22
cts 24
cp 0.9167
crap 12.0832
rs 6.9666

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
            $relationStatus = $tuple->state->getRelationStatus($relation->getName());
236 1230
            if ($relationStatus === 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
                $relationStatus = $tuple->state->getRelationStatus($relation->getName());
246 920
            } else {
247 864
                if ($tuple->status === Tuple::STATUS_PREPARING) {
248 864
                    if ($relationStatus === 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
                        $relationStatus = $tuple->state->getRelationStatus($relation->getName());
258 1728
                    }
259
                } else {
260
                    $relation->queue($this->pool, $tuple);
261
                    $relationStatus = $tuple->state->getRelationStatus($relation->getName());
262 2080
                }
263
            }
264
            $resolved = $resolved && $relationStatus >= RelationInterface::STATUS_DEFERRED;
265 2860
            $deferred = $deferred || $relationStatus === RelationInterface::STATUS_DEFERRED;
266
        }
267 2860
268 2206
        // $tuple->waitKeys = array_unique(array_merge(...$waitKeys));
269
        return ($deferred ? self::RELATIONS_DEFERRED : 0) | ($resolved ? self::RELATIONS_RESOLVED : 0);
270 1718
    }
271
272
    private function resolveSlaveRelations(Tuple $tuple, RelationMap $map): int
273 1718
    {
274 1718
        if (!$map->hasSlaves()) {
275 1718
            return self::RELATIONS_RESOLVED;
276 1718
        }
277 552
        $changedFields = \array_keys($tuple->state->getChanges());
278
279 1718
        // Attach children to pool
280 1718
        $transactData = $tuple->state->getTransactionData();
281 1718
        $deferred = false;
282 716
        $resolved = true;
283
        if ($tuple->status === Tuple::STATUS_PREPARING) {
284
            $relData = $tuple->mapper->fetchRelations($tuple->entity);
285 1718
        }
286 1718
        foreach ($map->getSlaves() as $name => $relation) {
287 1718
            $relationStatus = $tuple->state->getRelationStatus($relation->getName());
288 1718
            if (!$relation->isCascade() || $relationStatus === RelationInterface::STATUS_RESOLVED) {
289 1718
                continue;
290 1718
            }
291 1718
292
            $innerKeys = $relation->getInnerKeys();
293 1718
            $isWaitingKeys = \array_intersect($innerKeys, $tuple->state->getWaitingFields(true)) !== [];
294 1718
            $hasChangedKeys = \array_intersect($innerKeys, $changedFields) !== [];
295
            if ($relationStatus === RelationInterface::STATUS_PREPARE) {
296 1718
                $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...
297
                $relation->prepare(
298
                    $this->pool,
299 1718
                    $tuple,
300 1718
                    \array_key_exists($name, $relData)
301
                        ? $relData[$name]
302
                        : ($this->ignoreUninitializedRelations ? SpecialValue::notSet() : null),
303 1718
                    $isWaitingKeys || $hasChangedKeys,
304
                );
305 1322
                $relationStatus = $tuple->state->getRelationStatus($relation->getName());
306 1322
            }
307 1322
308
            if ($relationStatus !== RelationInterface::STATUS_PREPARE
309 1718
                && $relationStatus !== RelationInterface::STATUS_RESOLVED
310 1718
                && !$isWaitingKeys
311
                && !$hasChangedKeys
312
                && \count(\array_intersect($innerKeys, \array_keys($transactData))) === \count($innerKeys)
313 1718
            ) {
314
                // $child ??= $tuple->state->getRelation($name);
315
                $relation->queue($this->pool, $tuple);
316 2764
                $relationStatus = $tuple->state->getRelationStatus($relation->getName());
317
            }
318 2764
            $resolved = $resolved && $relationStatus === RelationInterface::STATUS_RESOLVED;
319 1916
            $deferred = $deferred || $relationStatus === RelationInterface::STATUS_DEFERRED;
320 1892
        }
321 88
322
        return ($deferred ? self::RELATIONS_DEFERRED : 0) | ($resolved ? self::RELATIONS_RESOLVED : 0);
323 1916
    }
324
325 2396
    private function resolveSelfWithEmbedded(Tuple $tuple, RelationMap $map, bool $hasDeferredRelations): void
326
    {
327 2396
        if (!$map->hasEmbedded() && !$tuple->state->hasChanges()) {
328
            $tuple->status = !$hasDeferredRelations
329 2228
                ? Tuple::STATUS_PROCESSED
330
                : \max(Tuple::STATUS_DEFERRED, $tuple->status);
331 2204
332 368
            return;
333 2084
        }
334
        $command = $this->commandGenerator->generateStoreCommand($this->orm, $tuple);
335 2204
336
        if (!$map->hasEmbedded()) {
337
            // Not embedded but has changes
338 168
            $this->runCommand($command);
339 168
340 168
            $tuple->status = $tuple->status <= Tuple::STATUS_PROPOSED && $hasDeferredRelations
341 168
                ? Tuple::STATUS_DEFERRED
342
                : Tuple::STATUS_PROCESSED;
343
344 168
            return;
345
        }
346 168
347 168
        $entityData = $tuple->mapper->extract($tuple->entity);
348
        foreach ($map->getEmbedded() as $name => $relation) {
349 168
            $relationStatus = $tuple->state->getRelationStatus($relation->getName());
350
            if ($relationStatus === RelationInterface::STATUS_RESOLVED) {
351
                continue;
352 152
            }
353
            $tuple->state->setRelation($name, $entityData[$name] ?? null);
354 152
            // We can use class MergeCommand here
355 152
            $relation->queue(
356
                $this->pool,
357
                $tuple,
358
                $command instanceof Sequence ? $command->getPrimaryCommand() : $command,
359 2900
            );
360
        }
361 2900
        $this->runCommand($command);
362
363
        $tuple->status = $tuple->status === Tuple::STATUS_PREPROCESSED || !$hasDeferredRelations
364 2900
            ? Tuple::STATUS_PROCESSED
365 2796
            : \max(Tuple::STATUS_DEFERRED, $tuple->status);
366 496
    }
367 2900
368 2900
    private function resolveRelations(Tuple $tuple): void
369
    {
370
        $map = $this->orm->getRelationMap($tuple->node->getRole());
371 2900
372 486
        $result = $tuple->task === Tuple::TASK_STORE
373
            ? $this->resolveMasterRelations($tuple, $map)
374
            : $this->resolveSlaveRelations($tuple, $map);
375 2900
        $isDependenciesResolved = (bool) ($result & self::RELATIONS_RESOLVED);
376 2868
        $deferred = (bool) ($result & self::RELATIONS_DEFERRED);
377 2764
378 496
        // Self
379 496
        if ($deferred && $tuple->status < Tuple::STATUS_PROPOSED) {
380
            $tuple->status = Tuple::STATUS_DEFERRED;
381 496
        }
382 496
383 480
        if ($isDependenciesResolved) {
384
            if ($tuple->task === Tuple::TASK_STORE) {
385
                $this->resolveSelfWithEmbedded($tuple, $map, $deferred);
386
            } elseif ($tuple->status === Tuple::STATUS_PREPARING) {
387 2860
                $tuple->status = Tuple::STATUS_WAITING;
388
            } else {
389 2860
                $command = $this->commandGenerator->generateDeleteCommand($this->orm, $tuple);
390 2756
                $this->runCommand($command);
391 496
                $tuple->status = Tuple::STATUS_PROCESSED;
392
            }
393
        }
394 2860
395 24
        if ($tuple->cascade) {
396
            $tuple->task === Tuple::TASK_STORE
397
                ? $this->resolveSlaveRelations($tuple, $map)
398
                : $this->resolveMasterRelations($tuple, $map);
399
        }
400
401
        if (!$isDependenciesResolved && $tuple->status === Tuple::STATUS_PREPROCESSED) {
402
            $tuple->status = Tuple::STATUS_UNPROCESSED;
403
        }
404
    }
405
}
406