Completed
Push — master ( 043491...033c1a )
by Nate
02:58
created

JsonDecodeReader   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 410
Duplicated Lines 26.1 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 45
lcom 1
cbo 3
dl 107
loc 410
ccs 146
cts 146
cp 1
rs 8.3673
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A beginArray() 8 8 1
A endArray() 0 7 1
A beginObject() 0 6 1
A endObject() 0 7 1
A hasNext() 0 6 2
A nextBoolean() 0 10 1
A nextDouble() 0 10 1
A nextInteger() 0 10 1
A nextString() 15 15 2
A nextNull() 0 10 1
A nextName() 16 16 1
C peek() 10 59 15
A skipValue() 0 4 1
B getPath() 15 15 6
A push() 11 11 2
A pop() 8 8 1
A expect() 14 14 2
A incrementPathIndex() 10 10 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like JsonDecodeReader 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 JsonDecodeReader, and based on these observations, apply Extract Interface, too.

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
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
0 ignored issues
show
Documentation introduced by
Should the type for parameter $type not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
369
     */
370 83 View Code Duplication
    private function push($element, $type = null): void
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
371
    {
372 83
        if (null === $type) {
373 83
            $type = gettype($element);
1 ignored issue
show
Coding Style introduced by
Consider using a different name than the parameter $type. This often makes code more readable.
Loading history...
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()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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