1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace Cycle\ORM\Heap; |
||||
6 | |||||
7 | final class Heap implements HeapInterface, \IteratorAggregate |
||||
8 | { |
||||
9 | private const INDEX_KEY_SEPARATOR = ':'; |
||||
10 | |||||
11 | private ?\SplObjectStorage $storage = null; |
||||
12 | private array $paths = []; |
||||
13 | |||||
14 | public function __construct() |
||||
15 | { |
||||
16 | $this->clean(); |
||||
17 | } |
||||
18 | |||||
19 | 7508 | public function getIterator(): \SplObjectStorage |
|||
20 | { |
||||
21 | 7508 | return $this->storage; |
|||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||||
22 | } |
||||
23 | |||||
24 | 7496 | public function has(object $entity): bool |
|||
25 | { |
||||
26 | 7496 | return $this->storage->offsetExists($entity); |
|||
0 ignored issues
–
show
The method
offsetExists() 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
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. ![]() |
|||||
27 | } |
||||
28 | |||||
29 | 7200 | public function get(object $entity): ?Node |
|||
30 | { |
||||
31 | 7200 | try { |
|||
32 | return $this->storage->offsetGet($entity); |
||||
33 | } catch (\UnexpectedValueException) { |
||||
34 | 7408 | return null; |
|||
35 | } |
||||
36 | 7408 | } |
|||
37 | |||||
38 | public function find(string $role, array $scope): ?object |
||||
39 | 356 | { |
|||
40 | if (!\array_key_exists($role, $this->paths) || $this->paths[$role] === []) { |
||||
41 | 356 | return null; |
|||
42 | } |
||||
43 | |||||
44 | 5328 | $isComposite = false; |
|||
45 | switch (\count($scope)) { |
||||
46 | case 0: |
||||
47 | 5328 | return null; |
|||
48 | 1658 | case 1: |
|||
49 | 1658 | $indexName = \key($scope); |
|||
50 | break; |
||||
51 | default: |
||||
52 | $isComposite = true; |
||||
53 | 4830 | $indexName = \implode(self::INDEX_KEY_SEPARATOR, \array_keys($scope)); |
|||
54 | } |
||||
55 | 4830 | ||||
56 | 4688 | if (!$isComposite) { |
|||
57 | $value = (string) \current($scope); |
||||
58 | return $this->paths[$role][$indexName][$value] ?? null; |
||||
59 | 3018 | } |
|||
60 | 3018 | $result = null; |
|||
61 | 3018 | // Find index |
|||
62 | 8 | if (!\array_key_exists($indexName, $this->paths[$role])) { |
|||
63 | 3014 | $scopeKeys = \array_keys($scope); |
|||
64 | 2766 | $scopeCount = \count($scopeKeys); |
|||
65 | 2766 | foreach ($this->paths[$role] as $indexName => $values) { |
|||
66 | $indexKeys = \explode(self::INDEX_KEY_SEPARATOR, $indexName); |
||||
67 | 370 | $keysCount = \count($indexKeys); |
|||
68 | 370 | if ($keysCount <= $scopeCount && \count(\array_intersect($indexKeys, $scopeKeys)) === $keysCount) { |
|||
69 | $result = &$this->paths[$role][$indexName]; |
||||
70 | break; |
||||
71 | 3014 | } |
|||
72 | 2766 | } |
|||
73 | 2766 | // Index not found |
|||
74 | if ($result === null) { |
||||
75 | 370 | return null; |
|||
76 | } |
||||
77 | 370 | } else { |
|||
78 | 92 | $result = &$this->paths[$role][$indexName]; |
|||
79 | 92 | } |
|||
80 | 92 | $indexKeys ??= \explode(self::INDEX_KEY_SEPARATOR, $indexName); |
|||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
81 | 92 | foreach ($indexKeys as $key) { |
|||
82 | 92 | $value = (string) $scope[$key]; |
|||
83 | 92 | if (!isset($result[$value])) { |
|||
84 | 34 | return null; |
|||
85 | 34 | } |
|||
86 | $result = &$result[$value]; |
||||
87 | } |
||||
88 | return $result; |
||||
89 | 92 | } |
|||
90 | 92 | ||||
91 | public function attach(object $entity, Node $node, array $index = []): void |
||||
92 | { |
||||
93 | 310 | $this->storage->offsetSet($entity, $node); |
|||
94 | $role = $node->getRole(); |
||||
95 | 312 | ||||
96 | 312 | if ($node->hasState()) { |
|||
97 | 312 | $this->eraseIndexes($role, $node->getData(), $entity); |
|||
98 | 312 | $data = $node->getState()->getData(); |
|||
99 | 188 | } else { |
|||
100 | $data = $node->getData(); |
||||
101 | 298 | } |
|||
102 | |||||
103 | 160 | if ($data === []) { |
|||
104 | return; |
||||
105 | } |
||||
106 | 5450 | foreach ($index as $key) { |
|||
107 | $isComposite = false; |
||||
108 | 5450 | if (\is_array($key)) { |
|||
109 | 5450 | switch (\count($key)) { |
|||
110 | case 0: |
||||
111 | 5450 | continue 2; |
|||
112 | 2704 | case 1: |
|||
113 | 2704 | $indexName = \current($key); |
|||
114 | break; |
||||
115 | 5450 | default: |
|||
116 | $isComposite = true; |
||||
117 | $indexName = \implode(self::INDEX_KEY_SEPARATOR, $key); |
||||
118 | 5450 | } |
|||
119 | 1822 | } else { |
|||
120 | $indexName = $key; |
||||
121 | 5298 | } |
|||
122 | 5290 | ||||
123 | 5290 | $rolePath = &$this->paths[$role][$indexName]; |
|||
124 | 742 | ||||
125 | 742 | // composite key |
|||
126 | 4 | if ($isComposite) { |
|||
127 | 740 | foreach ($key as $k) { |
|||
128 | 90 | if (!isset($data[$k])) { |
|||
129 | 90 | continue 2; |
|||
130 | } |
||||
131 | 682 | $value = (string) $data[$k]; |
|||
132 | 740 | $rolePath = &$rolePath[$value]; |
|||
133 | } |
||||
134 | $rolePath = $entity; |
||||
135 | 4676 | } else { |
|||
136 | if (!isset($data[$indexName])) { |
||||
137 | continue; |
||||
138 | 5290 | } |
|||
139 | $value = (string) $data[$indexName]; |
||||
140 | $rolePath[$value] = $entity; |
||||
141 | 5290 | } |
|||
142 | 682 | } |
|||
143 | 682 | } |
|||
144 | 24 | ||||
145 | public function detach(object $entity): void |
||||
146 | 682 | { |
|||
147 | 682 | $node = $this->get($entity); |
|||
148 | if ($node === null) { |
||||
149 | 680 | return; |
|||
150 | } |
||||
151 | 4726 | ||||
152 | 144 | $role = $node->getRole(); |
|||
153 | |||||
154 | 4726 | // erase all the indexes |
|||
155 | 4726 | $this->eraseIndexes($role, $node->getData(), $entity); |
|||
156 | if ($node->hasState()) { |
||||
157 | $this->eraseIndexes($role, $node->getState()->getData(), $entity); |
||||
158 | } |
||||
159 | |||||
160 | 492 | $this->storage->offsetUnset($entity); |
|||
161 | } |
||||
162 | 492 | ||||
163 | 492 | public function clean(): void |
|||
164 | 4 | { |
|||
165 | $this->paths = []; |
||||
166 | $this->storage = new \SplObjectStorage(); |
||||
167 | 488 | } |
|||
168 | |||||
169 | public function __clone() |
||||
170 | 488 | { |
|||
171 | 488 | $this->storage = clone $this->storage; |
|||
172 | 468 | } |
|||
173 | |||||
174 | public function __destruct() |
||||
175 | 488 | { |
|||
176 | $this->clean(); |
||||
177 | } |
||||
178 | 7508 | ||||
179 | private function eraseIndexes(string $role, array $data, object $entity): void |
||||
180 | 7508 | { |
|||
181 | 7508 | if (!isset($this->paths[$role]) || empty($data)) { |
|||
182 | return; |
||||
183 | } |
||||
184 | 2832 | foreach ($this->paths[$role] as $index => &$values) { |
|||
185 | if (empty($values)) { |
||||
186 | 2832 | continue; |
|||
187 | 1638 | } |
|||
188 | $keys = \explode(self::INDEX_KEY_SEPARATOR, $index); |
||||
189 | 2202 | $j = \count($keys) - 1; |
|||
190 | 2202 | $next = &$values; |
|||
191 | 246 | $removeFrom = &$next; |
|||
192 | // Walk index |
||||
193 | 2186 | foreach ($keys as $i => $key) { |
|||
194 | 2186 | $value = isset($data[$key]) ? (string) $data[$key] : null; |
|||
195 | 2186 | if ($value === null || !isset($next[$value])) { |
|||
196 | 2186 | continue 2; |
|||
197 | } |
||||
198 | 2186 | $removeKey ??= $value; |
|||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
199 | 2186 | // If last key |
|||
200 | 2186 | if ($i === $j) { |
|||
201 | 346 | if ($next[$value] === $entity) { |
|||
202 | unset($removeFrom[$removeKey ?? $value]); |
||||
203 | 2178 | } |
|||
204 | break; |
||||
205 | 2178 | } |
|||
206 | 2178 | // Optimization to remove empty arrays |
|||
207 | 2174 | if (\count($next[$value]) > 1) { |
|||
208 | $removeFrom = &$next[$value]; |
||||
209 | 2178 | $removeKey = null; |
|||
210 | } |
||||
211 | $next = &$next[$value]; |
||||
212 | 408 | } |
|||
213 | 108 | } |
|||
214 | 108 | } |
|||
215 | } |
||||
216 |