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
![]() Comprehensibility
Best Practice
introduced
by
|
|||
298 | 9 | $parent = array_values($parent); |
|
0 ignored issues
–
show
|
|||
299 | } |
||
300 | } |
||
301 | } |
||
302 | |||
303 | 34 | return $ref; |
|
304 | } |
||
305 | |||
306 | } |
||
307 |