Pool   F
last analyzed

Complexity

Total Complexity 86

Size/Duplication

Total Lines 360
Duplicated Lines 0 %

Test Coverage

Coverage 86.63%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 169
dl 0
loc 360
ccs 149
cts 172
cp 0.8663
rs 2
c 1
b 0
f 0
wmc 86

16 Methods

Rating   Name   Duplication   Size   Complexity  
A activatePriorityStorage() 0 7 2
B smartAttachTuple() 0 24 11
A closeIterator() 0 6 1
A getUnresolved() 0 6 2
B updateTuple() 0 22 11
A getAllTuples() 0 5 3
A trashIt() 0 24 5
A attachDelete() 0 7 1
A someHappens() 0 3 1
B snap() 0 30 9
F openIterator() 0 101 29
A count() 0 3 2
A attachStore() 0 9 1
A offsetGet() 0 3 2
A attach() 0 30 5
A __construct() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Pool 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 Pool, 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\Exception\PoolException;
8
use Cycle\ORM\Heap\Node;
9
use Cycle\ORM\Heap\State;
10
use Cycle\ORM\ORMInterface;
11
use Cycle\ORM\Reference\ReferenceInterface;
12
use JetBrains\PhpStorm\ExpectedValues;
13
14
/**
15
 * @internal
16
 *
17
 * @psalm-suppress TypeDoesNotContainType
18
 * @psalm-suppress RedundantCondition
19
 */
20
final class Pool implements \Countable
21
{
22
    private TupleStorage $storage;
23
    private TupleStorage $all;
24
    private TupleStorage $priorityStorage;
25
26
    /**
27
     * @var Tuple[]
28
     *
29
     * @psalm-var list<Tuple>
30
     */
31
    private array $unprocessed;
32
33
    private bool $priorityEnabled = false;
34
    private bool $priorityAutoAttach = false;
35
    private int $happens = 0;
36
37
    /**
38
     * Indicates that Pool is now iterating
39
     */
40
    private bool $iterating = false;
41
42
    public function __construct(
43
        private ORMInterface $orm,
44
    ) {
45
        $this->storage = new TupleStorage();
46
        $this->all = new TupleStorage();
47 2918
    }
48
49
    public function someHappens(): void
50 2918
    {
51 2918
        ++$this->happens;
52
    }
53
54 2564
    public function attach(
55
        object $entity,
56 2564
        #[ExpectedValues(values: [Tuple::TASK_STORE, Tuple::TASK_DELETE, Tuple::TASK_FORCE_DELETE])]
57
        int $task,
58
        bool $cascade,
59 2918
        ?Node $node = null,
60
        ?State $state = null,
61
        ?int $status = null,
62
        bool $highPriority = false,
63
        bool $persist = false,
64
    ): Tuple {
65
        // Find existing
66
        $tuple = $this->offsetGet($entity);
67
        if ($tuple !== null) {
68
            $this->updateTuple($tuple, $task, $status, $cascade, $node, $state);
69
            if ($persist) {
70
                $this->snap($tuple, true);
71 2918
            }
72 2918
            return $tuple;
73 650
        }
74 650
75
        $tuple = new Tuple($task, $entity, $cascade, $status ?? Tuple::STATUS_PREPARING);
76
        if ($node !== null) {
77 650
            $tuple->node = $node;
78
        }
79
        if ($state !== null) {
80 2918
            $tuple->state = $state;
81 2918
        }
82
83
        return $this->smartAttachTuple($tuple, $highPriority, $persist);
84 2918
    }
85
86
    public function attachStore(
87
        object $entity,
88 2918
        bool $cascade,
89
        ?Node $node = null,
90
        ?State $state = null,
91 2918
        bool $highPriority = false,
92
        bool $persist = false,
93 2918
    ): Tuple {
94
        return $this->attach($entity, Tuple::TASK_STORE, $cascade, $node, $state, null, $highPriority, $persist);
95
    }
96
97 2918
    public function attachDelete(
98
        object $entity,
99
        bool $cascade,
100 2918
        ?Node $node = null,
101
        ?State $state = null,
102 2918
    ): Tuple {
103 1884
        return $this->attach($entity, Tuple::TASK_DELETE, $cascade, $node, $state);
104
    }
105
106 2918
    public function offsetGet(object $entity): ?Tuple
107 496
    {
108
        return $this->all->contains($entity) ? $this->all->getTuple($entity) : null;
109 2918
    }
110
111
    /**
112 2918
     * Smart iterator
113
     *
114 2918
     * @return \Traversable<object, Tuple>
115
     */
116
    public function openIterator(): \Traversable
117 2812
    {
118
        if ($this->iterating) {
119
            throw new \RuntimeException('Iterator is already open.');
120
        }
121
        $this->iterating = true;
122
        $this->activatePriorityStorage();
123
        $this->unprocessed = [];
124
125 2812
        // Snap all entities before store
126
        /** @var object $entity */
127
        foreach ($this->storage as $entity => $tuple) {
128 496
            $this->snap($tuple);
129
            if (!isset($tuple->node)) {
130
                $this->storage->detach($entity);
131
            }
132
        }
133
134 496
        $stage = 0;
135
        do {
136
            // High priority first
137 2918
            if ($this->priorityStorage->count() > 0) {
138
                $priorityStorage = $this->priorityStorage;
139 2918
                foreach ($priorityStorage as $entity => $tuple) {
140
                    yield $entity => $tuple;
141
                    $this->trashIt($entity, $tuple, $priorityStorage);
142
                }
143
                continue;
144
            }
145
            // Other
146
            if ($this->storage->count() === 0) {
147 2916
                break;
148
            }
149 2916
            $pool = $this->storage;
150
            if ($stage === 0) {
151
                foreach ($this->storage as $entity => $tuple) {
152 2916
                    if ($tuple->status !== Tuple::STATUS_PREPARING) {
153 2916
                        continue;
154 2916
                    }
155
156
                    yield $entity => $tuple;
157 2916
                    $this->trashIt($entity, $tuple, $pool);
158
                    // Check priority
159 2900
                    if ($this->priorityStorage->count() > 0) {
160 2900
                        continue 2;
161 2900
                    }
162 80
                }
163
                $this->priorityAutoAttach = true;
164 2900
                $stage = 1;
165
            }
166
            if ($stage === 1) {
167
                /** @var object $entity */
168 2916
                foreach ($this->storage as $entity => $tuple) {
169
                    if ($tuple->status !== Tuple::STATUS_WAITING || $tuple->task === Tuple::TASK_DELETE) {
170
                        continue;
171 2916
                    }
172
173
                    $tuple->status = Tuple::STATUS_WAITED;
174
                    yield $entity => $tuple;
175
                    $this->trashIt($entity, $tuple, $this->storage);
176
                    // Check priority
177
                    if ($this->priorityStorage->count() > 0) {
178
                        continue 2;
179
                    }
180
                }
181 2916
                $stage = 2;
182 2804
            }
183
            if ($stage === 2) {
184 2900
                $this->happens = 0;
185 2900
                /** @var object $entity */
186 2900
                foreach ($this->storage as $entity => $tuple) {
187
                    if ($tuple->task === Tuple::TASK_DELETE) {
188 2900
                        $tuple->task = Tuple::TASK_FORCE_DELETE;
189
                    }
190 2900
                    if ($tuple->status === Tuple::STATUS_WAITING) {
191
                        $tuple->status = Tuple::STATUS_WAITED;
192 2900
                    } elseif ($tuple->status === Tuple::STATUS_DEFERRED) {
193 2900
                        $tuple->status = Tuple::STATUS_PROPOSED;
194 2900
                    }
195 2900
                    yield $entity => $tuple;
196 1808
                    $this->trashIt($entity, $tuple, $this->storage);
197
                    // Check priority
198 2900
                    if ($this->priorityStorage->count() > 0) {
199 2860
                        continue 2;
200
                    }
201 2860
                }
202
                $hasUnresolved = $this->unprocessed !== [];
203
                if ($this->happens !== 0 && $hasUnresolved) {
204
                    /** @psalm-suppress InvalidIterator */
205 2852
                    foreach ($this->unprocessed as $item) {
206 2852
                        $this->storage->attach($item);
207 2852
                    }
208
                    $this->unprocessed = [];
209 2852
                    continue;
210 2852
                }
211
                if ($this->happens === 0 && (\count($pool) > 0 || $hasUnresolved)) {
212 1824
                    throw new PoolException('Pool has gone into an infinite loop.');
213 1824
                }
214 1824
            }
215 1824
        } while (true);
216 957
        $this->closeIterator();
217
    }
218 1364
219 1364
    public function count(): int
220 1348
    {
221
        return \count($this->storage) + ($this->priorityEnabled ? $this->priorityStorage->count() : 0);
222 1348
    }
223
224
    /**
225
     * Get unresolved entity tuples.
226 2852
     *
227 2852
     * @return iterable<Tuple>
228
     */
229 2852
    public function getUnresolved(): iterable
230 2852
    {
231 2852
        if ($this->iterating) {
232
            return $this->unprocessed;
233 957
        }
234 957
        throw new PoolException('The Pool iterator isn\'t open.');
235 957
    }
236 328
237
    /**
238 957
     * @return iterable<object, Tuple> All valid tuples
239 371
     */
240 693
    public function getAllTuples(): iterable
241 605
    {
242
        foreach ($this->all as $entity => $tuple) {
243 957
            if (isset($tuple->node)) {
244 957
                yield $entity => $tuple;
245 949
            }
246
        }
247 949
    }
248
249
    /**
250
     * Close opened iterator
251 2812
     */
252 2812
    public function closeIterator(): void
253
    {
254 24
        $this->iterating = false;
255 24
        $this->priorityEnabled = false;
256
        $this->priorityAutoAttach = false;
257 24
        unset($this->priorityStorage, $this->unprocessed);
258 24
    }
259
260 2812
    private function smartAttachTuple(Tuple $tuple, bool $highPriority = false, bool $snap = false): Tuple
261 24
    {
262
        if ($tuple->status === Tuple::STATUS_PROCESSED) {
263
            $this->all->attach($tuple);
264
            return $tuple;
265 2804
        }
266
        if ($tuple->status === Tuple::STATUS_PREPARING && $this->all->contains($tuple->entity)) {
267
            return $this->all->getTuple($tuple->entity);
268
        }
269
        $this->all->attach($tuple);
270
271
        if ($this->iterating || $snap) {
272
            $this->snap($tuple);
273
        }
274
275
        if (isset($tuple->node) && $tuple->task === Tuple::TASK_DELETE) {
276
            $tuple->state->setStatus(Node::SCHEDULED_DELETE);
277
        }
278 24
        if (($this->priorityAutoAttach || $highPriority) && $tuple->status === Tuple::STATUS_PREPARING) {
279
            $this->priorityStorage->attach($tuple);
280 24
        } else {
281 24
            $this->storage->attach($tuple);
282
        }
283
        return $tuple;
284
    }
285
286
    /**
287
     * Make snapshot: create Node, State if not exists. Also attach Mapper
288
     */
289 2916
    private function snap(Tuple $tuple, bool $forceUpdateState = false): void
290
    {
291 2916
        $entity = $tuple->entity;
292 2900
        /** @var Node|null $node */
293 2900
        $node = $tuple->node ?? $this->orm->getHeap()->get($entity);
294 2900
295
        if (($node === null && $tuple->task !== Tuple::TASK_STORE) || $entity instanceof ReferenceInterface) {
296
            return;
297
        }
298
        $tuple->mapper ??= $this->orm->getMapper($node?->getRole() ?? $entity);
299
        if ($node === null) {
300
            // Create new Node
301
            $node = new Node(Node::NEW, [], $tuple->mapper->getRole());
302 2916
            if (isset($tuple->state)) {
303
                $tuple->state->setData($tuple->mapper->fetchFields($entity));
304 2916
                $node->setState($tuple->state);
305 2916
            }
306 2916
            $this->orm->getHeap()->attach($entity, $node);
307 2916
        }
308
        $tuple->node = $node;
309
        if (!isset($tuple->state)) {
310
            $tuple->state = $tuple->node->createState();
311
            $tuple->state->setData($tuple->mapper->fetchFields($entity));
312
        } elseif ($forceUpdateState) {
313
            $tuple->state->setData($tuple->mapper->fetchFields($entity));
314 2916
        }
315
316 2916
        // Backup State
317
        if (!$this->iterating) {
318 2916
            $tuple->persist = clone $tuple->state;
319
        }
320 2916
    }
321 80
322
    private function trashIt(object $entity, Tuple $tuple, TupleStorage $storage): void
323 2916
    {
324 2916
        if ($tuple->status === Tuple::STATUS_UNPROCESSED) {
325
            $storage->detach($entity);
326 1578
            $tuple->status = Tuple::STATUS_PREPROCESSED;
327 1578
            $this->unprocessed[] = $tuple;
328
            return;
329
        }
330
331 1578
        if ($tuple->status >= Tuple::STATUS_PREPROCESSED) {
332
            $storage->detach($entity);
333 2916
            $tuple->status = Tuple::STATUS_PROCESSED;
334 2916
            ++$this->happens;
335 2916
            return;
336 2916
        }
337
338
        if ($tuple->status % 2 === 0) {
339
            ++$tuple->status;
340 2916
            ++$this->happens;
341 80
        }
342
343
        if ($storage !== $this->storage) {
344
            $storage->detach($entity);
345 2860
            $this->storage->attach($tuple);
346
        }
347 2860
    }
348
349 2860
    private function activatePriorityStorage(): void
350 24
    {
351 24
        if ($this->priorityEnabled === true) {
352 24
            return;
353
        }
354
        $this->priorityEnabled = true;
355 2860
        $this->priorityStorage = new TupleStorage();
356 2828
    }
357 2828
358 2828
    private function updateTuple(Tuple $tuple, int $task, ?int $status, bool $cascade, ?Node $node, ?State $state): void
359
    {
360
        if ($status !== null && $tuple->status !== $status) {
361 1824
            if ($status === Tuple::STATUS_PROCESSED) {
362 1196
                $this->storage->detach($tuple->entity);
363 1196
                return;
364
            }
365 1824
            if ($tuple->status === Tuple::STATUS_PREPARING) {
366
                $tuple->status = $status;
367
            }
368 2916
        }
369
        if ($tuple->task !== $task) {
370 2916
            if ($tuple->task === Tuple::TASK_DELETE) {
371
                $tuple->task = $task;
372
            } elseif ($task === Tuple::TASK_FORCE_DELETE) {
373 2916
                $tuple->task = $task;
374 2916
            }
375
        }
376
377 650
        $tuple->cascade = $tuple->cascade || $cascade;
378
        $node === null or $tuple->node = $tuple->node ?? $node;
379 650
        $state === null or $tuple->state = $tuple->state ?? $state;
380
    }
381
}
382