UnitOfWork   F
last analyzed

Complexity

Total Complexity 77

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Test Coverage

Coverage 95.14%

Importance

Changes 4
Bugs 0 Features 1
Metric Value
eloc 192
c 4
b 0
f 1
dl 0
loc 372
ccs 176
cts 185
cp 0.9514
rs 2.24
wmc 77

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
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 setRunner() 0 3 1
A runCommand() 0 7 2
B resolveMasterRelations() 0 38 11
A run() 0 47 3
A checkActionPossibility() 0 4 3
A walkPool() 0 17 4
C resolveSelfWithEmbedded() 0 41 12
B resolveRelations() 0 35 11
D resolveSlaveRelations() 0 49 17

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