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\UnexpectedJsonTokenException; |
12
|
|
|
use Tebru\Gson\JsonReadable; |
13
|
|
|
use Tebru\Gson\JsonToken; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* Class JsonDecodeReader |
17
|
|
|
* |
18
|
|
|
* @author Nate Brunette <[email protected]> |
19
|
|
|
*/ |
20
|
|
|
final class JsonDecodeReader implements JsonReadable |
21
|
|
|
{ |
22
|
|
|
/** |
23
|
|
|
* A stack representing the next element to be consumed |
24
|
|
|
* |
25
|
|
|
* @var array |
26
|
|
|
*/ |
27
|
|
|
private $stack = []; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* An array of types that map to the position in the stack |
31
|
|
|
* |
32
|
|
|
* @var array |
33
|
|
|
*/ |
34
|
|
|
private $stackTypes = []; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* The current size of the stack |
38
|
|
|
* |
39
|
|
|
* @var int |
40
|
|
|
*/ |
41
|
|
|
private $stackSize = 0; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* A cache of the current [@see JsonToken]. This should get nulled out |
45
|
|
|
* whenever a new token should be returned with the subsequent call |
46
|
|
|
* to [@see JsonDecodeReader::peek] |
47
|
|
|
* |
48
|
|
|
* @var |
49
|
|
|
*/ |
50
|
|
|
private $currentToken; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Constructor |
54
|
|
|
* |
55
|
|
|
* @param string $json |
56
|
|
|
*/ |
57
|
69 |
|
public function __construct(string $json) |
58
|
|
|
{ |
59
|
69 |
|
$this->push(json_decode($json)); |
60
|
69 |
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Consumes the next token and asserts it's the beginning of a new array |
64
|
|
|
* |
65
|
|
|
* @return void |
66
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BEGIN_ARRAY |
67
|
|
|
*/ |
68
|
15 |
|
public function beginArray(): void |
69
|
|
|
{ |
70
|
15 |
|
if ($this->peek() !== JsonToken::BEGIN_ARRAY) { |
71
|
1 |
|
throw new UnexpectedJsonTokenException( |
72
|
1 |
|
sprintf('Expected "%s", but found "%s"', JsonToken::BEGIN_ARRAY, $this->peek()) |
73
|
|
|
); |
74
|
|
|
} |
75
|
|
|
|
76
|
14 |
|
$array = $this->pop(); |
77
|
14 |
|
$this->push(new ArrayIterator($array), ArrayIterator::class); |
78
|
14 |
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Consumes the next token and asserts it's the end of an array |
82
|
|
|
* |
83
|
|
|
* @return void |
84
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not END_ARRAY |
85
|
|
|
*/ |
86
|
5 |
|
public function endArray(): void |
87
|
|
|
{ |
88
|
5 |
|
if ($this->peek() !== JsonToken::END_ARRAY) { |
89
|
1 |
|
throw new UnexpectedJsonTokenException( |
90
|
1 |
|
sprintf('Expected "%s", but found "%s"', JsonToken::END_ARRAY, $this->peek()) |
91
|
|
|
); |
92
|
|
|
} |
93
|
|
|
|
94
|
4 |
|
$this->pop(); |
95
|
4 |
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Consumes the next token and asserts it's the beginning of a new object |
99
|
|
|
* |
100
|
|
|
* @return void |
101
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BEGIN_OBJECT |
102
|
|
|
*/ |
103
|
17 |
|
public function beginObject(): void |
104
|
|
|
{ |
105
|
17 |
|
if ($this->peek() !== JsonToken::BEGIN_OBJECT) { |
106
|
1 |
|
throw new UnexpectedJsonTokenException( |
107
|
1 |
|
sprintf('Expected "%s", but found "%s"', JsonToken::BEGIN_OBJECT, $this->peek()) |
108
|
|
|
); |
109
|
|
|
} |
110
|
|
|
|
111
|
16 |
|
$this->push(new StdClassIterator($this->pop()), StdClassIterator::class); |
112
|
16 |
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Consumes the next token and asserts it's the end of an object |
116
|
|
|
* |
117
|
|
|
* @return void |
118
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not END_OBJECT |
119
|
|
|
*/ |
120
|
4 |
|
public function endObject(): void |
121
|
|
|
{ |
122
|
4 |
|
if ($this->peek() !== JsonToken::END_OBJECT) { |
123
|
1 |
|
throw new UnexpectedJsonTokenException( |
124
|
1 |
|
sprintf('Expected "%s", but found "%s"', JsonToken::END_OBJECT, $this->peek()) |
125
|
|
|
); |
126
|
|
|
} |
127
|
|
|
|
128
|
3 |
|
$this->pop(); |
129
|
3 |
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Returns true if the array or object has another element |
133
|
|
|
* |
134
|
|
|
* If the current scope is not an array or object, this returns false |
135
|
|
|
* |
136
|
|
|
* @return bool |
137
|
|
|
*/ |
138
|
4 |
|
public function hasNext(): bool |
139
|
|
|
{ |
140
|
4 |
|
$peek = $this->peek(); |
141
|
|
|
|
142
|
4 |
|
return $peek !== JsonToken::END_OBJECT && $peek !== JsonToken::END_ARRAY; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Consumes the value of the next token, asserts it's a boolean and returns it |
147
|
|
|
* |
148
|
|
|
* @return bool |
149
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BOOLEAN |
150
|
|
|
*/ |
151
|
6 |
View Code Duplication |
public function nextBoolean(): bool |
|
|
|
|
152
|
|
|
{ |
153
|
6 |
|
if ($this->peek() !== JsonToken::BOOLEAN) { |
154
|
1 |
|
throw new UnexpectedJsonTokenException( |
155
|
1 |
|
sprintf('Expected "%s", but found "%s"', JsonToken::BOOLEAN, $this->peek()) |
156
|
|
|
); |
157
|
|
|
} |
158
|
|
|
|
159
|
5 |
|
return $this->pop(); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Consumes the value of the next token, asserts it's a double and returns it |
164
|
|
|
* |
165
|
|
|
* @return double |
166
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NUMBER |
167
|
|
|
*/ |
168
|
3 |
View Code Duplication |
public function nextDouble(): float |
|
|
|
|
169
|
|
|
{ |
170
|
3 |
|
if ($this->peek() !== JsonToken::NUMBER) { |
171
|
1 |
|
throw new UnexpectedJsonTokenException( |
172
|
1 |
|
sprintf('Expected "%s", but found "%s"', JsonToken::NUMBER, $this->peek()) |
173
|
|
|
); |
174
|
|
|
} |
175
|
|
|
|
176
|
2 |
|
return (float)$this->pop(); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Consumes the value of the next token, asserts it's an int and returns it |
181
|
|
|
* |
182
|
|
|
* @return int |
183
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NUMBER |
184
|
|
|
*/ |
185
|
7 |
View Code Duplication |
public function nextInteger(): int |
|
|
|
|
186
|
|
|
{ |
187
|
7 |
|
if ($this->peek() !== JsonToken::NUMBER) { |
188
|
1 |
|
throw new UnexpectedJsonTokenException( |
189
|
1 |
|
sprintf('Expected "%s", but found "%s"', JsonToken::NUMBER, $this->peek()) |
190
|
|
|
); |
191
|
|
|
} |
192
|
|
|
|
193
|
6 |
|
return (int)$this->pop(); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Consumes the value of the next token, asserts it's a string and returns it |
198
|
|
|
* |
199
|
|
|
* @return string |
200
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME or STRING |
201
|
|
|
*/ |
202
|
13 |
View Code Duplication |
public function nextString(): string |
|
|
|
|
203
|
|
|
{ |
204
|
13 |
|
$peek = $this->peek(); |
205
|
13 |
|
if ($peek === JsonToken::NAME) { |
206
|
1 |
|
return $this->nextName(); |
207
|
|
|
} |
208
|
|
|
|
209
|
12 |
|
if ($peek !== JsonToken::STRING) { |
210
|
1 |
|
throw new UnexpectedJsonTokenException( |
211
|
1 |
|
sprintf('Expected "%s", but found "%s"', JsonToken::STRING, $this->peek()) |
212
|
|
|
); |
213
|
|
|
} |
214
|
|
|
|
215
|
11 |
|
return $this->pop(); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Consumes the value of the next token and asserts it's null |
220
|
|
|
* |
221
|
|
|
* @return null |
222
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME or NULL |
223
|
|
|
*/ |
224
|
2 |
View Code Duplication |
public function nextNull() |
|
|
|
|
225
|
|
|
{ |
226
|
2 |
|
if ($this->peek() !== JsonToken::NULL) { |
227
|
1 |
|
throw new UnexpectedJsonTokenException( |
228
|
1 |
|
sprintf('Expected "%s", but found "%s"', JsonToken::NULL, $this->peek()) |
229
|
|
|
); |
230
|
|
|
} |
231
|
|
|
|
232
|
1 |
|
$this->pop(); |
233
|
|
|
|
234
|
1 |
|
return null; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Consumes the next name and returns it |
239
|
|
|
* |
240
|
|
|
* @return string |
241
|
|
|
* @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME |
242
|
|
|
*/ |
243
|
9 |
View Code Duplication |
public function nextName(): string |
|
|
|
|
244
|
|
|
{ |
245
|
9 |
|
if ($this->peek() !== JsonToken::NAME) { |
246
|
1 |
|
throw new UnexpectedJsonTokenException( |
247
|
1 |
|
sprintf('Expected "%s", but found "%s"', JsonToken::NAME, $this->peek()) |
248
|
|
|
); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** @var StdClassIterator $iterator */ |
252
|
9 |
|
$iterator = $this->stack[$this->stackSize - 1]; |
253
|
9 |
|
$key = $iterator->key(); |
254
|
9 |
|
$value = $iterator->current(); |
255
|
9 |
|
$iterator->next(); |
256
|
|
|
|
257
|
9 |
|
$this->push($value); |
258
|
|
|
|
259
|
9 |
|
return $key; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Returns an enum representing the type of the next token without consuming it |
264
|
|
|
* |
265
|
|
|
* @return string |
266
|
|
|
*/ |
267
|
69 |
|
public function peek(): string |
268
|
|
|
{ |
269
|
69 |
|
if (null !== $this->currentToken) { |
270
|
11 |
|
return $this->currentToken; |
271
|
|
|
} |
272
|
|
|
|
273
|
69 |
|
if (0 === $this->stackSize) { |
274
|
1 |
|
$this->currentToken = JsonToken::END_DOCUMENT; |
275
|
|
|
|
276
|
1 |
|
return $this->currentToken; |
277
|
|
|
} |
278
|
|
|
|
279
|
69 |
|
$token = null; |
280
|
69 |
|
$element = $this->stack[$this->stackSize - 1]; |
281
|
|
|
|
282
|
69 |
|
switch ($this->stackTypes[$this->stackSize - 1]) { |
283
|
69 |
|
case 'array': |
284
|
16 |
|
$token = JsonToken::BEGIN_ARRAY; |
285
|
16 |
|
break; |
286
|
66 |
|
case 'string': |
287
|
18 |
|
$token = JsonToken::STRING; |
288
|
18 |
|
break; |
289
|
51 |
|
case 'double': |
290
|
1 |
|
$token = JsonToken::NUMBER; |
291
|
1 |
|
break; |
292
|
50 |
|
case 'integer': |
293
|
21 |
|
$token = JsonToken::NUMBER; |
294
|
21 |
|
break; |
295
|
36 |
|
case 'boolean': |
296
|
7 |
|
return JsonToken::BOOLEAN; |
297
|
32 |
|
case 'NULL': |
298
|
2 |
|
$token = JsonToken::NULL; |
299
|
2 |
|
break; |
300
|
30 |
|
case StdClassIterator::class: |
301
|
15 |
|
$token = $element->valid() ? JsonToken::NAME : JsonToken::END_OBJECT; |
302
|
15 |
|
break; |
303
|
30 |
View Code Duplication |
case ArrayIterator::class: |
|
|
|
|
304
|
13 |
|
if ($element->valid()) { |
305
|
9 |
|
$this->push($element->current()); |
306
|
9 |
|
$element->next(); |
307
|
|
|
|
308
|
9 |
|
$token = $this->peek(); |
309
|
|
|
} else { |
310
|
7 |
|
$token = JsonToken::END_ARRAY; |
311
|
|
|
} |
312
|
13 |
|
break; |
313
|
20 |
|
case 'object': |
314
|
20 |
|
switch (get_class($element)) { |
315
|
20 |
|
case stdClass::class: |
316
|
20 |
|
$token = JsonToken::BEGIN_OBJECT; |
317
|
20 |
|
break; |
318
|
|
|
} |
319
|
20 |
|
break; |
320
|
|
|
} |
321
|
|
|
|
322
|
65 |
|
$this->currentToken = $token; |
323
|
|
|
|
324
|
65 |
|
return $this->currentToken; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* Skip the next value. If the next value is an object or array, all children will |
329
|
|
|
* also be skipped. |
330
|
|
|
* |
331
|
|
|
* @return void |
332
|
|
|
*/ |
333
|
1 |
|
public function skipValue(): void |
334
|
|
|
{ |
335
|
1 |
|
$this->pop(); |
336
|
1 |
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* Push an element onto the stack |
340
|
|
|
* |
341
|
|
|
* @param mixed $element |
342
|
|
|
* @param string $type |
|
|
|
|
343
|
|
|
*/ |
344
|
69 |
View Code Duplication |
private function push($element, $type = null): void |
|
|
|
|
345
|
|
|
{ |
346
|
69 |
|
if (null === $type) { |
347
|
69 |
|
$type = gettype($element); |
|
|
|
|
348
|
|
|
} |
349
|
|
|
|
350
|
69 |
|
$this->stack[$this->stackSize] = $element; |
351
|
69 |
|
$this->stackTypes[$this->stackSize] = $type; |
352
|
69 |
|
$this->stackSize++; |
353
|
69 |
|
$this->currentToken = null; |
354
|
69 |
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* Pop the last element off the stack and return it |
358
|
|
|
* |
359
|
|
|
* @return mixed |
360
|
|
|
*/ |
361
|
45 |
View Code Duplication |
private function pop() |
|
|
|
|
362
|
|
|
{ |
363
|
45 |
|
$this->stackSize--; |
364
|
45 |
|
array_pop($this->stackTypes); |
365
|
45 |
|
$this->currentToken = null; |
366
|
|
|
|
367
|
45 |
|
return array_pop($this->stack); |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
|
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.