Heap   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 206
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 117
dl 0
loc 206
ccs 117
cts 117
cp 1
rs 8.64
c 2
b 0
f 1
wmc 47

11 Methods

Rating   Name   Duplication   Size   Complexity  
A detach() 0 16 3
C eraseIndexes() 0 33 12
C find() 0 51 13
A has() 0 3 1
A __clone() 0 3 1
A __destruct() 0 3 1
A clean() 0 4 1
A __construct() 0 3 1
B attach() 0 50 11
A get() 0 6 2
A getIterator() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Heap 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 Heap, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\ORM\Heap;
6
7
use IteratorAggregate;
8
use SplObjectStorage;
9
use UnexpectedValueException;
10
11
final class Heap implements HeapInterface, IteratorAggregate
12
{
13
    private const INDEX_KEY_SEPARATOR = ':';
14
15
    private ?SplObjectStorage $storage = null;
16
17
    private array $paths = [];
18
19 7508
    public function __construct()
20
    {
21 7508
        $this->clean();
22
    }
23
24 7496
    public function __destruct()
25
    {
26 7496
        $this->clean();
27
    }
28
29 7200
    public function __clone()
30
    {
31 7200
        $this->storage = clone $this->storage;
32
    }
33
34 7408
    public function getIterator(): SplObjectStorage
35
    {
36 7408
        return $this->storage;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->storage could return the type null which is incompatible with the type-hinted return SplObjectStorage. Consider adding an additional type-check to rule them out.
Loading history...
37
    }
38
39 356
    public function has(object $entity): bool
40
    {
41 356
        return $this->storage->offsetExists($entity);
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

41
        return $this->storage->/** @scrutinizer ignore-call */ offsetExists($entity);

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.

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