1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* Copyright (c) Nate Brunette. |
4
|
|
|
* Distributed under the MIT License (http://opensource.org/licenses/MIT) |
5
|
|
|
*/ |
6
|
|
|
|
7
|
|
|
namespace Tebru\Gson\Internal; |
8
|
|
|
|
9
|
|
|
use ArrayIterator; |
10
|
|
|
use stdClass; |
11
|
|
|
use Tebru\Gson\Exception\MalformedJsonException; |
12
|
|
|
use Tebru\Gson\Exception\UnexpectedJsonTokenException; |
13
|
|
|
use Tebru\Gson\JsonReadable; |
14
|
|
|
use Tebru\Gson\JsonToken; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Class JsonDecodeReader |
18
|
|
|
* |
19
|
|
|
* @author Nate Brunette <[email protected]> |
20
|
|
|
*/ |
21
|
|
|
final class JsonDecodeReader implements JsonReadable |
22
|
|
|
{ |
23
|
|
|
/** |
24
|
|
|
* A stack representing the next element to be consumed |
25
|
|
|
* |
26
|
|
|
* @var array |
27
|
|
|
*/ |
28
|
|
|
private $stack = []; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* An array of types that map to the position in the stack |
32
|
|
|
* |
33
|
|
|
* @var array |
34
|
|
|
*/ |
35
|
|
|
private $stackTypes = []; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* The current size of the stack |
39
|
|
|
* |
40
|
|
|
* @var int |
41
|
|
|
*/ |
42
|
|
|
private $stackSize = 0; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* An array of path names that correspond to the current stack |
46
|
|
|
* |
47
|
|
|
* @var array |
48
|
|
|
*/ |
49
|
|
|
private $pathNames = []; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* An array of path indicies that correspond to the current stack. This array could contain invalid |
53
|
|
|
* values at indexes outside the current stack. It could also contain incorrect values at indexes |
54
|
|
|
* where a path name is used. Data should only be fetched by referencing the current position in the stack. |
55
|
|
|
* |
56
|
|
|
* @var array |
57
|
|
|
*/ |
58
|
|
|
private $pathIndices = []; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* A cache of the current [@see JsonToken]. This should get nulled out |
62
|
|
|
* whenever a new token should be returned with the subsequent call |
63
|
|
|
* to [@see JsonDecodeReader::peek] |
64
|
|
|
* |
65
|
|
|
* @var |
66
|
|
|
*/ |
67
|
|
|
private $currentToken; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Constructor |
71
|
|
|
* |
72
|
|
|
* @param string $json |
73
|
|
|
* @throws \Tebru\Gson\Exception\MalformedJsonException If the json cannot be decoded |
74
|
|
|
*/ |
75
|
83 |
|
public function __construct(string $json) |
76
|
|
|
{ |
77
|
83 |
|
$this->push(json_decode($json)); |
78
|
|
|
|
79
|
83 |
|
if (json_last_error() !== JSON_ERROR_NONE) { |
80
|
1 |
|
throw new MalformedJsonException(sprintf('Could not decode json, the error message was: "%s"', json_last_error_msg())); |
81
|
|
|
} |
82
|
82 |
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Consumes the next token and asserts it's the beginning of a new array |
86
|
|
|
* |
87
|
|
|
* @return void |
88
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BEGIN_ARRAY |
89
|
|
|
*/ |
90
|
24 |
View Code Duplication |
public function beginArray(): void |
|
|
|
|
91
|
|
|
{ |
92
|
24 |
|
$this->expect(JsonToken::BEGIN_ARRAY); |
93
|
|
|
|
94
|
23 |
|
$array = $this->pop(); |
95
|
23 |
|
$this->push(new ArrayIterator($array), ArrayIterator::class); |
96
|
23 |
|
$this->pathIndices[$this->stackSize - 1] = 0; |
97
|
23 |
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Consumes the next token and asserts it's the end of an array |
101
|
|
|
* |
102
|
|
|
* @return void |
103
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not END_ARRAY |
104
|
|
|
*/ |
105
|
7 |
|
public function endArray(): void |
106
|
|
|
{ |
107
|
7 |
|
$this->expect(JsonToken::END_ARRAY); |
108
|
|
|
|
109
|
6 |
|
$this->pop(); |
110
|
6 |
|
$this->incrementPathIndex(); |
111
|
6 |
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Consumes the next token and asserts it's the beginning of a new object |
115
|
|
|
* |
116
|
|
|
* @return void |
117
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BEGIN_OBJECT |
118
|
|
|
*/ |
119
|
24 |
|
public function beginObject(): void |
120
|
|
|
{ |
121
|
24 |
|
$this->expect(JsonToken::BEGIN_OBJECT); |
122
|
|
|
|
123
|
23 |
|
$this->push(new StdClassIterator($this->pop()), StdClassIterator::class); |
124
|
23 |
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Consumes the next token and asserts it's the end of an object |
128
|
|
|
* |
129
|
|
|
* @return void |
130
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not END_OBJECT |
131
|
|
|
*/ |
132
|
6 |
|
public function endObject(): void |
133
|
|
|
{ |
134
|
6 |
|
$this->expect(JsonToken::END_OBJECT); |
135
|
|
|
|
136
|
5 |
|
$this->pop(); |
137
|
5 |
|
$this->incrementPathIndex(); |
138
|
5 |
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Returns true if the array or object has another element |
142
|
|
|
* |
143
|
|
|
* If the current scope is not an array or object, this returns false |
144
|
|
|
* |
145
|
|
|
* @return bool |
146
|
|
|
*/ |
147
|
4 |
|
public function hasNext(): bool |
148
|
|
|
{ |
149
|
4 |
|
$peek = $this->peek(); |
150
|
|
|
|
151
|
4 |
|
return $peek !== JsonToken::END_OBJECT && $peek !== JsonToken::END_ARRAY; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Consumes the value of the next token, asserts it's a boolean and returns it |
156
|
|
|
* |
157
|
|
|
* @return bool |
158
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BOOLEAN |
159
|
|
|
*/ |
160
|
6 |
|
public function nextBoolean(): bool |
161
|
|
|
{ |
162
|
6 |
|
$this->expect(JsonToken::BOOLEAN); |
163
|
|
|
|
164
|
6 |
|
$result = (bool)$this->pop(); |
165
|
|
|
|
166
|
6 |
|
$this->incrementPathIndex(); |
167
|
|
|
|
168
|
6 |
|
return $result; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Consumes the value of the next token, asserts it's a double and returns it |
173
|
|
|
* |
174
|
|
|
* @return double |
175
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NUMBER |
176
|
|
|
*/ |
177
|
3 |
|
public function nextDouble(): float |
178
|
|
|
{ |
179
|
3 |
|
$this->expect(JsonToken::NUMBER); |
180
|
|
|
|
181
|
2 |
|
$result = (float)$this->pop(); |
182
|
|
|
|
183
|
2 |
|
$this->incrementPathIndex(); |
184
|
|
|
|
185
|
2 |
|
return $result; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Consumes the value of the next token, asserts it's an int and returns it |
190
|
|
|
* |
191
|
|
|
* @return int |
192
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NUMBER |
193
|
|
|
*/ |
194
|
15 |
|
public function nextInteger(): int |
195
|
|
|
{ |
196
|
15 |
|
$this->expect(JsonToken::NUMBER); |
197
|
|
|
|
198
|
14 |
|
$result = (int)$this->pop(); |
199
|
|
|
|
200
|
14 |
|
$this->incrementPathIndex(); |
201
|
|
|
|
202
|
14 |
|
return $result; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Consumes the value of the next token, asserts it's a string and returns it |
207
|
|
|
* |
208
|
|
|
* @return string |
209
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME or STRING |
210
|
|
|
*/ |
211
|
13 |
View Code Duplication |
public function nextString(): string |
|
|
|
|
212
|
|
|
{ |
213
|
13 |
|
$peek = $this->peek(); |
214
|
13 |
|
if ($peek === JsonToken::NAME) { |
215
|
1 |
|
return $this->nextName(); |
216
|
|
|
} |
217
|
|
|
|
218
|
12 |
|
$this->expect(JsonToken::STRING); |
219
|
|
|
|
220
|
11 |
|
$result = (string)$this->pop(); |
221
|
|
|
|
222
|
11 |
|
$this->incrementPathIndex(); |
223
|
|
|
|
224
|
11 |
|
return $result; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Consumes the value of the next token and asserts it's null |
229
|
|
|
* |
230
|
|
|
* @return null |
231
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME or NULL |
232
|
|
|
*/ |
233
|
2 |
|
public function nextNull() |
234
|
|
|
{ |
235
|
2 |
|
$this->expect(JsonToken::NULL); |
236
|
|
|
|
237
|
1 |
|
$this->pop(); |
238
|
|
|
|
239
|
1 |
|
$this->incrementPathIndex(); |
240
|
|
|
|
241
|
1 |
|
return null; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* Consumes the next name and returns it |
246
|
|
|
* |
247
|
|
|
* @return string |
248
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME |
249
|
|
|
*/ |
250
|
16 |
View Code Duplication |
public function nextName(): string |
|
|
|
|
251
|
|
|
{ |
252
|
16 |
|
$this->expect(JsonToken::NAME); |
253
|
|
|
|
254
|
|
|
/** @var StdClassIterator $iterator */ |
255
|
16 |
|
$iterator = $this->stack[$this->stackSize - 1]; |
256
|
16 |
|
$key = $iterator->key(); |
257
|
16 |
|
$value = $iterator->current(); |
258
|
16 |
|
$iterator->next(); |
259
|
|
|
|
260
|
16 |
|
$this->pathNames[$this->stackSize - 1] = $key; |
261
|
|
|
|
262
|
16 |
|
$this->push($value); |
263
|
|
|
|
264
|
16 |
|
return (string)$key; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* Returns an enum representing the type of the next token without consuming it |
269
|
|
|
* |
270
|
|
|
* @return string |
271
|
|
|
*/ |
272
|
81 |
|
public function peek(): string |
273
|
|
|
{ |
274
|
81 |
|
if (null !== $this->currentToken) { |
275
|
22 |
|
return $this->currentToken; |
276
|
|
|
} |
277
|
|
|
|
278
|
81 |
|
if (0 === $this->stackSize) { |
279
|
1 |
|
$this->currentToken = JsonToken::END_DOCUMENT; |
280
|
|
|
|
281
|
1 |
|
return $this->currentToken; |
282
|
|
|
} |
283
|
|
|
|
284
|
81 |
|
$token = null; |
285
|
81 |
|
$element = $this->stack[$this->stackSize - 1]; |
286
|
|
|
|
287
|
81 |
|
switch ($this->stackTypes[$this->stackSize - 1]) { |
288
|
81 |
|
case 'array': |
289
|
25 |
|
$token = JsonToken::BEGIN_ARRAY; |
290
|
25 |
|
break; |
291
|
77 |
|
case 'string': |
292
|
18 |
|
$token = JsonToken::STRING; |
293
|
18 |
|
break; |
294
|
65 |
|
case 'double': |
295
|
1 |
|
$token = JsonToken::NUMBER; |
296
|
1 |
|
break; |
297
|
64 |
|
case 'integer': |
298
|
30 |
|
$token = JsonToken::NUMBER; |
299
|
30 |
|
break; |
300
|
50 |
|
case 'boolean': |
301
|
8 |
|
return JsonToken::BOOLEAN; |
302
|
46 |
|
case 'NULL': |
303
|
2 |
|
$token = JsonToken::NULL; |
304
|
2 |
|
break; |
305
|
44 |
|
case StdClassIterator::class: |
306
|
21 |
|
$token = $element->valid() ? JsonToken::NAME : JsonToken::END_OBJECT; |
307
|
21 |
|
break; |
308
|
44 |
View Code Duplication |
case ArrayIterator::class: |
|
|
|
|
309
|
21 |
|
if ($element->valid()) { |
310
|
17 |
|
$this->push($element->current()); |
311
|
17 |
|
$element->next(); |
312
|
|
|
|
313
|
17 |
|
$token = $this->peek(); |
314
|
|
|
} else { |
315
|
9 |
|
$token = JsonToken::END_ARRAY; |
316
|
|
|
} |
317
|
21 |
|
break; |
318
|
27 |
|
case 'object': |
319
|
27 |
|
switch (get_class($element)) { |
320
|
27 |
|
case stdClass::class: |
321
|
27 |
|
$token = JsonToken::BEGIN_OBJECT; |
322
|
27 |
|
break; |
323
|
|
|
} |
324
|
27 |
|
break; |
325
|
|
|
} |
326
|
|
|
|
327
|
77 |
|
$this->currentToken = $token; |
328
|
|
|
|
329
|
77 |
|
return $this->currentToken; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* Skip the next value. If the next value is an object or array, all children will |
334
|
|
|
* also be skipped. |
335
|
|
|
* |
336
|
|
|
* @return void |
337
|
|
|
*/ |
338
|
1 |
|
public function skipValue(): void |
339
|
|
|
{ |
340
|
1 |
|
$this->pop(); |
341
|
1 |
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Get the current read path in json xpath format |
345
|
|
|
* |
346
|
|
|
* @return string |
347
|
|
|
*/ |
348
|
23 |
View Code Duplication |
public function getPath(): string |
|
|
|
|
349
|
|
|
{ |
350
|
23 |
|
$result = '$'; |
351
|
23 |
|
foreach ($this->stack as $index => $item) { |
352
|
21 |
|
if ($item instanceof ArrayIterator && isset($this->pathIndices[$index])) { |
353
|
9 |
|
$result .= '['.$this->pathIndices[$index].']'; |
354
|
|
|
} |
355
|
|
|
|
356
|
21 |
|
if ($item instanceof StdClassIterator && isset($this->pathNames[$index])) { |
357
|
21 |
|
$result .= '.'.$this->pathNames[$index]; |
358
|
|
|
} |
359
|
|
|
} |
360
|
|
|
|
361
|
23 |
|
return $result; |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* Push an element onto the stack |
366
|
|
|
* |
367
|
|
|
* @param mixed $element |
368
|
|
|
* @param string $type |
|
|
|
|
369
|
|
|
*/ |
370
|
83 |
View Code Duplication |
private function push($element, $type = null): void |
|
|
|
|
371
|
|
|
{ |
372
|
83 |
|
if (null === $type) { |
373
|
83 |
|
$type = gettype($element); |
|
|
|
|
374
|
|
|
} |
375
|
|
|
|
376
|
83 |
|
$this->stack[$this->stackSize] = $element; |
377
|
83 |
|
$this->stackTypes[$this->stackSize] = $type; |
378
|
83 |
|
$this->stackSize++; |
379
|
83 |
|
$this->currentToken = null; |
380
|
83 |
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* Pop the last element off the stack and return it |
384
|
|
|
* |
385
|
|
|
* @return mixed |
386
|
|
|
*/ |
387
|
60 |
View Code Duplication |
private function pop() |
|
|
|
|
388
|
|
|
{ |
389
|
60 |
|
$this->stackSize--; |
390
|
60 |
|
array_pop($this->stackTypes); |
391
|
60 |
|
$this->currentToken = null; |
392
|
|
|
|
393
|
60 |
|
return array_pop($this->stack); |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
/** |
397
|
|
|
* Check that the next token equals the expectation |
398
|
|
|
* |
399
|
|
|
* @param string $expectedToken |
400
|
|
|
* @throws UnexpectedJsonTokenException |
401
|
|
|
*/ |
402
|
64 |
View Code Duplication |
private function expect(string $expectedToken) |
|
|
|
|
403
|
|
|
{ |
404
|
64 |
|
if ($this->peek() === $expectedToken) { |
405
|
60 |
|
return; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
// increment the path index because exceptions are thrown before this value is increased. We |
409
|
|
|
// want to display the current index that has a problem. |
410
|
10 |
|
$this->incrementPathIndex(); |
411
|
|
|
|
412
|
10 |
|
throw new UnexpectedJsonTokenException( |
413
|
10 |
|
sprintf('Expected "%s", but found "%s" at "%s"', $expectedToken, $this->peek(), $this->getPath()) |
414
|
|
|
); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* Increment the path index. This should be called any time a new value is parsed. |
419
|
|
|
*/ |
420
|
44 |
View Code Duplication |
private function incrementPathIndex(): void |
|
|
|
|
421
|
|
|
{ |
422
|
44 |
|
$index = $this->stackSize - 1; |
423
|
44 |
|
if ($index >= 0) { |
424
|
25 |
|
if (!isset($this->pathIndices[$index])) { |
425
|
17 |
|
$this->pathIndices[$index] = 0; |
426
|
|
|
} |
427
|
25 |
|
$this->pathIndices[$index]++; |
428
|
|
|
} |
429
|
44 |
|
} |
430
|
|
|
} |
431
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.