UnitOfWork   F
last analyzed

Complexity

Total Complexity 81

Size/Duplication

Total Lines 381
Duplicated Lines 0 %

Test Coverage

Coverage 95.19%

Importance

Changes 5
Bugs 0 Features 1
Metric Value
eloc 201
c 5
b 0
f 1
dl 0
loc 381
ccs 178
cts 187
cp 0.9519
rs 2
wmc 81

18 Methods

Rating   Name   Duplication   Size   Complexity  
A delete() 0 6 1
A retry() 0 3 1
A isSuccess() 0 3 1
A getLastError() 0 3 1
A persistState() 0 6 1
A persistDeferred() 0 6 1
A resetHeap() 0 4 2
A syncHeap() 0 30 4
A __construct() 0 7 1
A setRunner() 0 3 1
A runCommand() 0 7 2
B resolveRelations() 0 35 11
C resolveMasterRelations() 0 44 13
A run() 0 47 3
A checkActionPossibility() 0 4 3
D resolveSlaveRelations() 0 51 19
A walkPool() 0 17 4
C resolveSelfWithEmbedded() 0 41 12

How to fix   Complexity   

Complex Class

Complex classes like UnitOfWork often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UnitOfWork, and based on these observations, apply Extract Interface, too.

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