Heap   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 205
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 117
dl 0
loc 205
ccs 113
cts 113
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
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
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...
22
    }
23
24 7496
    public function has(object $entity): bool
25
    {
26 7496
        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

26
        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...
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
The variable $indexKeys does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
The variable $removeKey does not seem to be defined for all execution paths leading up to this point.
Loading history...
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