Pool::openIterator()   F
last analyzed

Complexity

Conditions 29
Paths 1099

Size

Total Lines 101
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 56
CRAP Score 31.2343

Importance

Changes 0
Metric Value
cc 29
dl 0
loc 101
rs 0
c 0
b 0
f 0
eloc 63
nc 1099
nop 0
ccs 56
cts 65
cp 0.8615
crap 31.2343

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