Passed
Push — master ( 1173f0...070f3f )
by Nate
13:41
created

JsonDecodeReader   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 350
Duplicated Lines 29.71 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 2
dl 104
loc 350
ccs 130
cts 130
cp 1
rs 8.3157
c 0
b 0
f 0

16 Methods

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

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\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
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...
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
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...
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
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...
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
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...
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()
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...
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
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...
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:
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...
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
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...
343
     */
344 69 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...
345
    {
346 69
        if (null === $type) {
347 69
            $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...
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()
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...
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