Passed
Pull Request — 2.x (#517)
by Aleksei
18:45
created

UnitOfWork::resolveSelfWithEmbedded()   C

Complexity

Conditions 12
Paths 18

Size

Total Lines 41
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 12.0737

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

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