Passed
Push — 2.x ( 0b5227...cb81b7 )
by butschster
16:17
created

Pool::attach()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 30
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.3073

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 12
nc 6
nop 8
dl 0
loc 30
ccs 10
cts 13
cp 0.7692
crap 5.3073
rs 9.5555
c 1
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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