Passed
Push — 2.x ( b81303...ac0a18 )
by Aleksei
16:00
created

Pool::attachDelete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 4
dl 0
loc 7
ccs 2
cts 2
cp 1
crap 1
rs 10
c 1
b 0
f 0
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 2910
    public function __construct(
48
        private ORMInterface $orm
49
    ) {
50 2910
        $this->storage = new SplObjectStorage();
51 2910
        $this->all = new SplObjectStorage();
52 2910
    }
53
54 2556
    public function someHappens(): void
55
    {
56 2556
        ++$this->happens;
57 2556
    }
58
59 2910
    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 2910
        $tuple = $this->offsetGet($entity);
72 2910
        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 2910
        $tuple = new Tuple($task, $entity, $cascade, $status ?? Tuple::STATUS_PREPARING);
81 2910
        if ($node !== null) {
82
            $tuple->node = $node;
83
        }
84 2910
        if ($state !== null) {
85
            $tuple->state = $state;
86
        }
87
88 2910
        return $this->smartAttachTuple($tuple, $highPriority, $persist);
89
    }
90
91 2910
    private function smartAttachTuple(Tuple $tuple, bool $highPriority = false, bool $snap = false): Tuple
92
    {
93 2910
        if ($tuple->status === Tuple::STATUS_PROCESSED) {
94
            $this->all->attach($tuple->entity, $tuple);
95
            return $tuple;
96
        }
97 2910
        if ($tuple->status === Tuple::STATUS_PREPARING && $this->all->contains($tuple->entity)) {
98
            return $this->all->offsetGet($tuple->entity);
99
        }
100 2910
        $this->all->attach($tuple->entity, $tuple);
101
102 2910
        if ($this->iterating || $snap) {
103 1876
            $this->snap($tuple);
104
        }
105
106 2910
        if (isset($tuple->node) && $tuple->task === Tuple::TASK_DELETE) {
107 496
            $tuple->state->setStatus(Node::SCHEDULED_DELETE);
108
        }
109 2910
        if (($this->priorityAutoAttach || $highPriority) && $tuple->status === Tuple::STATUS_PREPARING) {
110 1
            $this->priorityStorage->attach($tuple->entity, $tuple);
111
        } else {
112 2910
            $this->storage->attach($tuple->entity, $tuple);
113
        }
114 2910
        return $tuple;
115
    }
116
117 2804
    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 2804
        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 2910
    public function offsetGet(object $entity): ?Tuple
138
    {
139 2910
        return $this->all->contains($entity) ? $this->all->offsetGet($entity) : null;
140
    }
141
142
    /**
143
     * Smart iterator
144
     *
145
     * @return Traversable<object, Tuple>
146
     */
147 2908
    public function openIterator(): Traversable
148
    {
149 2908
        if ($this->iterating) {
150
            throw new \RuntimeException('Iterator is already open.');
151
        }
152 2908
        $this->iterating = true;
153 2908
        $this->activatePriorityStorage();
154 2908
        $this->unprocessed = [];
155
156
        // Snap all entities before store
157 2908
        while ($this->storage->valid()) {
158
            /** @var Tuple $tuple */
159 2892
            $tuple = $this->storage->getInfo();
160 2892
            $this->snap($tuple);
161 2892
            if (!isset($tuple->node)) {
162 80
                $this->storage->detach($this->storage->current());
163
            } else {
164 2892
                $this->storage->next();
165
            }
166
        }
167
168 2908
        $stage = 0;
169
        do {
170
            // High priority first
171 2908
            if ($this->priorityStorage->count() > 0) {
172 1
                $priorityStorage = $this->priorityStorage;
173 1
                foreach ($priorityStorage as $entity) {
174 1
                    $tuple = $priorityStorage->offsetGet($entity);
175 1
                    yield $entity => $tuple;
176 1
                    $this->trashIt($entity, $tuple, $priorityStorage);
177
                }
178 1
                continue;
179
            }
180
            // Other
181 2908
            if ($this->storage->count() === 0) {
182 2796
                break;
183
            }
184 2892
            $pool = $this->storage;
185 2892
            if (!$pool->valid() && $pool->count() > 0) {
186 2892
                $pool->rewind();
187
            }
188 2892
            if ($stage === 0) {
189
                // foreach ($pool as $entity) {
190 2892
                while ($pool->valid()) {
191
                    /** @var Tuple $tuple */
192 2892
                    $entity = $pool->current();
193 2892
                    $tuple = $pool->getInfo();
194 2892
                    $pool->next();
195 2892
                    if ($tuple->status !== Tuple::STATUS_PREPARING) {
196 1800
                        continue;
197
                    }
198 2892
                    yield $entity => $tuple;
199 2852
                    $this->trashIt($entity, $tuple, $this->storage);
200
                    // Check priority
201 2852
                    if ($this->priorityStorage->count() > 0) {
202
                        continue 2;
203
                    }
204
                }
205 2844
                $this->priorityAutoAttach = true;
206 2844
                $stage = 1;
207 2844
                $this->storage->rewind();
208
            }
209 2844
            if ($stage === 1) {
210 2844
                while ($pool->valid()) {
211
                    /** @var Tuple $tuple */
212 1816
                    $entity = $pool->current();
213 1816
                    $tuple = $pool->getInfo();
214 1816
                    $pool->next();
215 1816
                    if ($tuple->status !== Tuple::STATUS_WAITING || $tuple->task === Tuple::TASK_DELETE) {
216 955
                        continue;
217
                    }
218 1356
                    $tuple->status = Tuple::STATUS_WAITED;
219 1356
                    yield $entity => $tuple;
220 1340
                    $this->trashIt($entity, $tuple, $this->storage);
221
                    // Check priority
222 1340
                    if ($this->priorityStorage->count() > 0) {
223
                        continue 2;
224
                    }
225
                }
226 2844
                $stage = 2;
227 2844
                $this->storage->rewind();
228
            }
229 2844
            if ($stage === 2) {
230 2844
                $this->happens = 0;
231 2844
                while ($pool->valid()) {
232
                    /** @var Tuple $tuple */
233 956
                    $entity = $pool->current();
234 956
                    $tuple = $pool->getInfo();
235 956
                    if ($tuple->task === Tuple::TASK_DELETE) {
236 328
                        $tuple->task = Tuple::TASK_FORCE_DELETE;
237
                    }
238 956
                    if ($tuple->status === Tuple::STATUS_WAITING) {
239 370
                        $tuple->status = Tuple::STATUS_WAITED;
240 692
                    } elseif ($tuple->status === Tuple::STATUS_DEFERRED) {
241 604
                        $tuple->status = Tuple::STATUS_PROPOSED;
242
                    }
243 956
                    $pool->next();
244 956
                    yield $entity => $tuple;
245 948
                    $this->trashIt($entity, $tuple, $this->storage);
246
                    // Check priority
247 948
                    if ($this->priorityStorage->count() > 0) {
248 1
                        continue 2;
249
                    }
250
                }
251 2804
                $hasUnresolved = $this->unprocessed !== [];
252 2804
                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 2804
                if ($this->happens === 0 && (\count($pool) > 0 || $hasUnresolved)) {
261 24
                    throw new PoolException('Pool has gone into an infinite loop.');
262
                }
263
            }
264 2804
        } while (true);
265 2796
        $this->closeIterator();
266 2796
    }
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 2908
    public function getAllTuples(): iterable
290
    {
291 2908
        foreach ($this->all as $entity) {
292 2892
            $tuple = $this->all->offsetGet($entity);
293 2892
            if (isset($tuple->node)) {
294 2892
                yield $entity => $tuple;
295
            }
296
        }
297 2908
    }
298
299
    /**
300
     * Close opened iterator
301
     */
302 2908
    public function closeIterator(): void
303
    {
304 2908
        $this->iterating = false;
305 2908
        $this->priorityEnabled = false;
306 2908
        $this->priorityAutoAttach = false;
307 2908
        unset($this->priorityStorage, $this->unprocessed);
308
        // $this->all = new SplObjectStorage();
309 2908
    }
310
311
    /**
312
     * Make snapshot: create Node, State if not exists. Also attach Mapper
313
     */
314 2908
    private function snap(Tuple $tuple): void
315
    {
316 2908
        $entity = $tuple->entity;
317
        /** @var Node|null $node */
318 2908
        $node = $tuple->node ?? $this->orm->getHeap()->get($entity);
319
320 2908
        if (($node === null && $tuple->task !== Tuple::TASK_STORE) || $entity instanceof ReferenceInterface) {
321 80
            return;
322
        }
323 2908
        $tuple->mapper ??= $this->orm->getMapper($node?->getRole() ?? $entity);
324 2908
        if ($node === null) {
325
            // Create new Node
326 1570
            $node = new Node(Node::NEW, [], $tuple->mapper->getRole());
327 1570
            if (isset($tuple->state)) {
328
                $tuple->state->setData($tuple->mapper->fetchFields($entity));
329
                $node->setState($tuple->state);
330
            }
331 1570
            $this->orm->getHeap()->attach($entity, $node);
332
        }
333 2908
        $tuple->node = $node;
334 2908
        if (!isset($tuple->state)) {
335 2908
            $tuple->state = $tuple->node->createState();
336 2908
            $tuple->state->setData($tuple->mapper->fetchFields($entity));
337
        }
338
339
        // Backup State
340 2908
        if (!$this->iterating) {
341 80
            $tuple->persist = clone $tuple->state;
342
        }
343 2908
    }
344
345 2852
    private function trashIt(object $entity, Tuple $tuple, SplObjectStorage $storage): void
346
    {
347 2852
        $storage->detach($entity);
348
349 2852
        if ($tuple->status === Tuple::STATUS_UNPROCESSED) {
350 24
            $tuple->status = Tuple::STATUS_PREPROCESSED;
351 24
            $this->unprocessed[] = $tuple;
352 24
            return;
353
        }
354
355 2852
        if ($tuple->status >= Tuple::STATUS_PREPROCESSED) {
356 2820
            $tuple->status = Tuple::STATUS_PROCESSED;
357 2820
            ++$this->happens;
358 2820
            return;
359
        }
360
361 1816
        if ($tuple->status % 2 === 0) {
362 1188
            ++$tuple->status;
363 1188
            ++$this->happens;
364
        }
365 1816
        $this->storage->attach($tuple->entity, $tuple);
366 1816
    }
367
368 2908
    private function activatePriorityStorage(): void
369
    {
370 2908
        if ($this->priorityEnabled === true) {
371
            return;
372
        }
373 2908
        $this->priorityEnabled = true;
374 2908
        $this->priorityStorage = new SplObjectStorage();
375 2908
    }
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 650
    }
400
}
401