Completed
Push — master ( 1282c4...de18f5 )
by Viacheslav
17:22 queued 03:56
created

JsonPointer::buildPath()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 7
c 0
b 0
f 0
ccs 5
cts 5
cp 1
rs 10
cc 3
nc 4
nop 2
crap 3
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 Exception('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 < 71000 && '' === $key) {
108
                    throw new Exception('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 Exception('Non-existent path item: ' . $key);
117
                    } else {
118 45
                        $ref = &$ref->$key;
119
                    }
120
                }
121
            } else { // null or array
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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 Exception('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[];
138
                    } else {
139 40
                        if (is_array($ref) && array_key_exists($key, $ref) && empty($pathItems)) {
140 8
                            array_splice($ref, $key, 0, array($value));
141
                        }
142 40
                        if (false === $intKey) {
143 4
                            if (0 === ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS)) {
144 3
                                throw new Exception('Invalid key for array operation');
145
                            }
146 1
                            $ref = &$ref[$key];
147 1
                            continue;
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 Exception('Index is greater than number of items in array');
152 35
                            } elseif ($intKey < 0) {
153 1
                                throw new Exception('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 < 71000 && '' === $key) {
206
                    throw new Exception('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 Exception('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 Exception('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 Exception('Key not found: ' . $key);
227
                }
228
            } else {
229
                throw new Exception('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 Exception('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 Exception('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 Exception('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
                $ff = $flags & self::TOLERATE_ASSOCIATIVE_ARRAYS;
0 ignored issues
show
Unused Code introduced by
The assignment to $ff is dead and can be removed.
Loading history...
286 16
                if ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS) {
287 1
                    $i = 0;
288 1
                    foreach ($parent as $index => $value) {
289 1
                        if ($i !== $index) {
290 1
                            $isAssociative = true;
291 1
                            break;
292
                        }
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