JsonPointer   F
last analyzed

Complexity

Total Complexity 77

Size/Duplication

Total Lines 298
Duplicated Lines 0 %

Test Coverage

Coverage 94.24%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 149
c 4
b 0
f 0
dl 0
loc 298
ccs 131
cts 139
cp 0.9424
rs 2.24
wmc 77

11 Methods

Rating   Name   Duplication   Size   Complexity  
A splitPath() 0 11 3
A buildPath() 0 7 3
A splitPathJsonString() 0 8 2
A splitPathURIFragment() 0 8 2
B get() 0 33 10
A getByPointer() 0 3 1
A escapeSegment() 0 6 2
A arrayKeyExists() 0 12 4
A arrayGet() 0 9 3
D add() 0 64 31
C remove() 0 51 16

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace Swaggest\JsonDiff;
4
5
6
class JsonPointer
7
{
8
    /**
9
     * Create intermediate keys if they don't exist
10
     */
11
    const RECURSIVE_KEY_CREATION = 1;
12
13
    /**
14
     * Disallow converting empty array to object for key creation
15
     */
16
    const STRICT_MODE = 2;
17
18
    /**
19
     * Skip action if holder already has a non-null value at path
20
     */
21
    const SKIP_IF_ISSET = 4;
22
23
    /**
24
     * Allow associative arrays to mimic JSON objects (not recommended)
25
     */
26
    const TOLERATE_ASSOCIATIVE_ARRAYS = 8;
27
28
    /**
29
     * @param string $key
30
     * @param bool $isURIFragmentId
31
     * @return string
32
     */
33 41
    public static function escapeSegment($key, $isURIFragmentId = false)
34
    {
35 41
        if ($isURIFragmentId) {
36 3
            return str_replace(array('%7E', '%2F'), array('~0', '~1'), urlencode($key));
37
        } else {
38 39
            return str_replace(array('~', '/'), array('~0', '~1'), $key);
39
        }
40
    }
41
42
    /**
43
     * @param string[] $pathItems
44
     * @param bool $isURIFragmentId
45
     * @return string
46
     */
47 1
    public static function buildPath(array $pathItems, $isURIFragmentId = false)
48
    {
49 1
        $result = $isURIFragmentId ? '#' : '';
50 1
        foreach ($pathItems as $pathItem) {
51 1
            $result .= '/' . self::escapeSegment($pathItem, $isURIFragmentId);
52
        }
53 1
        return $result;
54
    }
55
56
    /**
57
     * @param string $path
58
     * @return string[]
59
     * @throws Exception
60
     */
61 96
    public static function splitPath($path)
62
    {
63 96
        $pathItems = explode('/', $path);
64 96
        $first = array_shift($pathItems);
65 96
        if ($first === '#') {
66 1
            return self::splitPathURIFragment($pathItems);
67
        } else {
68 96
            if ($first !== '') {
69
                throw new JsonPointerException('Path must start with "/": ' . $path);
70
            }
71 96
            return self::splitPathJsonString($pathItems);
72
        }
73
    }
74
75 1
    private static function splitPathURIFragment(array $pathItems)
76
    {
77 1
        $result = array();
78 1
        foreach ($pathItems as $key) {
79 1
            $key = str_replace(array('~1', '~0'), array('/', '~'), urldecode($key));
80 1
            $result[] = $key;
81
        }
82 1
        return $result;
83
    }
84
85 96
    private static function splitPathJsonString(array $pathItems)
86
    {
87 96
        $result = array();
88 96
        foreach ($pathItems as $key) {
89 92
            $key = str_replace(array('~1', '~0'), array('/', '~'), $key);
90 92
            $result[] = $key;
91
        }
92 96
        return $result;
93
    }
94
95
    /**
96
     * @param mixed $holder
97
     * @param string[] $pathItems
98
     * @param mixed $value
99
     * @param int $flags
100
     * @throws Exception
101
     */
102 92
    public static function add(&$holder, $pathItems, $value, $flags = self::RECURSIVE_KEY_CREATION)
103
    {
104 92
        $ref = &$holder;
105 92
        while (null !== $key = array_shift($pathItems)) {
106 84
            if ($ref instanceof \stdClass || is_object($ref)) {
107 48
                if (PHP_VERSION_ID < 70100 && '' === $key) {
108
                    throw new JsonPointerException('Empty property name is not supported by PHP <7.1',
109
                        Exception::EMPTY_PROPERTY_NAME_UNSUPPORTED);
110
                }
111
112 48
                if ($flags & self::RECURSIVE_KEY_CREATION) {
113 13
                    $ref = &$ref->$key;
114
                } else {
115 40
                    if (!isset($ref->$key) && count($pathItems)) {
116 3
                        throw new JsonPointerException('Non-existent path item: ' . $key);
117
                    } else {
118 45
                        $ref = &$ref->$key;
119
                    }
120
                }
121
            } else { // null or array
122 57
                $intKey = filter_var($key, FILTER_VALIDATE_INT);
123 57
                if ($ref === null && (false === $intKey || $intKey !== 0)) {
124 27
                    $key = (string)$key;
125 27
                    if ($flags & self::RECURSIVE_KEY_CREATION) {
126 27
                        $ref = new \stdClass();
127 27
                        $ref = &$ref->{$key};
128
                    } else {
129 27
                        throw new JsonPointerException('Non-existent path item: ' . $key);
130
                    }
131 43
                } elseif ([] === $ref && 0 === ($flags & self::STRICT_MODE) && false === $intKey && '-' !== $key) {
132 1
                    $ref = new \stdClass();
133 1
                    $ref = &$ref->{$key};
134
                } else {
135 43
                    if ($flags & self::RECURSIVE_KEY_CREATION && $ref === null) $ref = array();
136 43
                    if ('-' === $key) {
137 4
                        $ref = &$ref[count($ref)];
138
                    } else {
139 40
                        if (false === $intKey) {
140 8
                            if (0 === ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS)) {
141
                                throw new JsonPointerException('Invalid key for array operation');
142 40
                            }
143 4
                            $ref = &$ref[$key];
144 3
                            continue;
145
                        }
146 1
                        if (is_array($ref) && array_key_exists($key, $ref) && empty($pathItems)) {
147 1
                            array_splice($ref, $intKey, 0, array($value));
148
                        }
149 37
                        if (0 === ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS)) {
150 37
                            if ($intKey > count($ref) && 0 === ($flags & self::RECURSIVE_KEY_CREATION)) {
151 2
                                throw new JsonPointerException('Index is greater than number of items in array');
152 35
                            } elseif ($intKey < 0) {
153 1
                                throw new JsonPointerException('Negative index');
154
                            }
155
                        }
156
157 34
                        $ref = &$ref[$intKey];
158
                    }
159
                }
160
            }
161
        }
162 84
        if ($ref !== null && $flags & self::SKIP_IF_ISSET) {
163 1
            return;
164
        }
165 84
        $ref = $value;
166 84
    }
167
168 44
    private static function arrayKeyExists($key, array $a)
169
    {
170 44
        if (array_key_exists($key, $a)) {
171 40
            return true;
172
        }
173 8
        $key = (string)$key;
174 8
        foreach ($a as $k => $v) {
175 8
            if ((string)$k === $key) {
176
                return true;
177
            }
178
        }
179 8
        return false;
180
    }
181
182 31
    private static function arrayGet($key, array $a)
183
    {
184 31
        $key = (string)$key;
185 31
        foreach ($a as $k => $v) {
186 31
            if ((string)$k === $key) {
187 31
                return $v;
188
            }
189
        }
190
        return false;
191
    }
192
193
194
    /**
195
     * @param mixed $holder
196
     * @param string[] $pathItems
197
     * @return bool|mixed
198
     * @throws Exception
199
     */
200 46
    public static function get($holder, $pathItems)
201
    {
202 46
        $ref = $holder;
203 46
        while (null !== $key = array_shift($pathItems)) {
204 45
            if ($ref instanceof \stdClass) {
205 32
                if (PHP_VERSION_ID < 70100 && '' === $key) {
206
                    throw new JsonPointerException('Empty property name is not supported by PHP <7.1',
207
                        Exception::EMPTY_PROPERTY_NAME_UNSUPPORTED);
208
                }
209
210 32
                $vars = (array)$ref;
211 32
                if (self::arrayKeyExists($key, $vars)) {
212 31
                    $ref = self::arrayGet($key, $vars);
213
                } else {
214 32
                    throw new JsonPointerException('Key not found: ' . $key);
215
                }
216 24
            } elseif (is_array($ref)) {
217 23
                if (self::arrayKeyExists($key, $ref)) {
218 18
                    $ref = $ref[$key];
219
                } else {
220 23
                    throw new JsonPointerException('Key not found: ' . $key);
221
                }
222 1
            } elseif (is_object($ref)) {
223 1
                if (isset($ref->$key)) {
224 1
                    $ref = $ref->$key;
225
                } else {
226 1
                    throw new JsonPointerException('Key not found: ' . $key);
227
                }
228
            } else {
229
                throw new JsonPointerException('Key not found: ' . $key);
230
            }
231
        }
232 40
        return $ref;
233
    }
234
235
    /**
236
     * @param mixed $holder
237
     * @param string $pointer
238
     * @return bool|mixed
239
     * @throws Exception
240
     */
241 1
    public static function getByPointer($holder, $pointer)
242
    {
243 1
        return self::get($holder, self::splitPath($pointer));
244
    }
245
246
    /**
247
     * @param mixed $holder
248
     * @param string[] $pathItems
249
     * @param int $flags
250
     * @return mixed
251
     * @throws Exception
252
     */
253 38
    public static function remove(&$holder, $pathItems, $flags = 0)
254
    {
255 38
        $ref = &$holder;
256 38
        while (null !== $key = array_shift($pathItems)) {
257 37
            $parent = &$ref;
258 37
            $refKey = $key;
259 37
            if ($ref instanceof \stdClass) {
260 23
                if (property_exists($ref, $key)) {
261 22
                    $ref = &$ref->$key;
262
                } else {
263 23
                    throw new JsonPointerException('Key not found: ' . $key);
264
                }
265 24
            } elseif (is_object($ref)) {
266 1
                if (isset($ref->$key)) {
267 1
                    $ref = &$ref->$key;
268
                } else {
269 1
                    throw new JsonPointerException('Key not found: ' . $key);
270
                }
271
            } else {
272 23
                if (array_key_exists($key, $ref)) {
273 20
                    $ref = &$ref[$key];
274
                } else {
275 3
                    throw new JsonPointerException('Key not found: ' . $key);
276
                }
277
            }
278
        }
279
280 34
        if (isset($parent) && isset($refKey)) {
281 33
            if ($parent instanceof \stdClass || is_object($parent)) {
282 20
                unset($parent->$refKey);
283
            } else {
284 16
                $isAssociative = false;
285 16
                if ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS) {
286 16
                    $i = 0;
287 1
                    foreach ($parent as $index => $value) {
288 1
                        if ($i !== $index) {
289 1
                            $isAssociative = true;
290 1
                            break;
291 1
                        }
292
                        $i++;
293
                    }
294
                }
295
296 16
                unset($parent[$refKey]);
297 16
                if (!$isAssociative && (int)$refKey !== count($parent)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $refKey does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $parent does not seem to be defined for all execution paths leading up to this point.
Loading history...
298 9
                    $parent = array_values($parent);
0 ignored issues
show
Unused Code introduced by
The assignment to $parent is dead and can be removed.
Loading history...
299
                }
300
            }
301
        }
302
303 34
        return $ref;
304
    }
305
306
}
307