Completed
Pull Request — master (#31)
by
unknown
03:20
created

JsonDecoder   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 324
Duplicated Lines 19.14 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 87.23%

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 7
dl 62
loc 324
ccs 82
cts 94
cp 0.8723
rs 8.8
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
C decode() 7 26 7
B decodeFile() 13 60 7
A getMaxDepth() 0 4 1
A setMaxDepth() 18 18 4
A getObjectDecoding() 0 4 1
A setObjectDecoding() 12 12 3
A getBigIntDecoding() 0 4 1
A setBigIntDecoding() 12 12 3
C decodeJson() 0 34 7

How to fix   Duplicated Code   

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:

1
<?php
2
3
/*
4
 * This file is part of the Webmozart JSON package.
5
 *
6
 * (c) Bernhard Schussek <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Webmozart\Json;
13
14
use Seld\JsonLint\JsonParser;
15
use Seld\JsonLint\ParsingException;
16
17
/**
18
 * Decodes JSON strings/files and validates against a JSON schema.
19
 *
20
 * @since  1.0
21
 *
22
 * @author Bernhard Schussek <[email protected]>
23
 */
24
class JsonDecoder
25
{
26
    /**
27
     * Decode a JSON value as PHP object.
28
     */
29
    const OBJECT = 0;
30
31
    /**
32
     * Decode a JSON value as associative array.
33
     */
34
    const ASSOC_ARRAY = 1;
35
36
    /**
37
     * Decode a JSON value as float.
38
     */
39
    const FLOAT = 2;
40
41
    /**
42
     * Decode a JSON value as string.
43
     */
44
    const STRING = 3;
45
46
    /**
47
     * @var JsonValidator
48
     */
49
    private $validator;
50
51
    /**
52
     * @var int
53
     */
54
    private $objectDecoding = self::OBJECT;
55
56
    /**
57
     * @var int
58
     */
59
    private $bigIntDecoding = self::FLOAT;
60
61
    /**
62
     * @var int
63
     */
64
    private $maxDepth = 512;
65
66
    /**
67
     * Creates a new decoder.
68
     *
69
     * @param null|JsonValidator $validator
70
     */
71 36
    public function __construct(JsonValidator $validator = null)
72
    {
73 36
        $this->validator = $validator ?: new JsonValidator();
74 36
    }
75
76
    /**
77
     * Decodes and validates a JSON string.
78
     *
79
     * If a schema is passed, the decoded object is validated against that
80
     * schema. The schema may be passed as file path or as object returned from
81
     * `JsonDecoder::decodeFile($schemaFile)`.
82
     *
83
     * You can adjust the decoding with {@link setObjectDecoding()},
84
     * {@link setBigIntDecoding()} and {@link setMaxDepth()}.
85
     *
86
     * Schema validation is not supported when objects are decoded as
87
     * associative arrays.
88
     *
89
     * @param string        $json   The JSON string
90
     * @param string|object $schema The schema file or object
0 ignored issues
show
Documentation introduced by
Should the type for parameter $schema not be string|object|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...
91
     *
92
     * @return mixed The decoded value
93
     *
94
     * @throws DecodingFailedException   If the JSON string could not be decoded
95
     * @throws ValidationFailedException If the decoded string fails schema
96
     *                                   validation
97
     * @throws InvalidSchemaException    If the schema is invalid
98
     */
99 26
    public function decode($json, $schema = null)
100
    {
101 26
        if (self::ASSOC_ARRAY === $this->objectDecoding && null !== $schema) {
102 1
            throw new \InvalidArgumentException(
103
                'Schema validation is not supported when objects are decoded '.
104
                'as associative arrays. Call '.
105 1
                'JsonDecoder::setObjectDecoding(JsonDecoder::JSON_OBJECT) to fix.'
106
            );
107
        }
108
109 25
        $data = $this->decodeJson($json);
110
111 21
        if (null === $schema && isset($data->{'$schema'})) {
112
            $schema = $data->{'$schema'};
113
        }
114
115 21 View Code Duplication
        if (null !== $schema) {
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...
116 7
            $errors = $this->validator->validate($data, $schema);
117
118 6
            if (count($errors) > 0) {
119 4
                throw ValidationFailedException::fromErrors($errors);
120
            }
121
        }
122
123 16
        return $data;
124
    }
125
126
    /**
127
     * Decodes and validates a JSON file.
128
     *
129
     * @param string        $path   The path to the JSON file
130
     * @param string|object $schema The schema file or object
0 ignored issues
show
Documentation introduced by
Should the type for parameter $schema not be string|object|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...
131
     *
132
     * @return mixed The decoded file
133
     *
134
     * @throws FileNotFoundException     If the file was not found
135
     * @throws DecodingFailedException   If the file could not be decoded
136
     * @throws ValidationFailedException If the decoded file fails schema
137
     *                                   validation
138
     * @throws InvalidSchemaException    If the schema is invalid
139
     *
140
     * @see decode
141
     */
142 7
    public function decodeFile($path, $schema = null)
143
    {
144 7
        if (!file_exists($path)) {
145 1
            throw new FileNotFoundException(sprintf(
146 1
                'The file %s does not exist.',
147
                $path
148
            ));
149
        }
150
151 6
        $errorMessage = null;
152 6
        $errorCode = 0;
153
154 6
        set_error_handler(function ($errno, $errstr) use (&$errorMessage, &$errorCode) {
155 1
            $errorMessage = $errstr;
156 1
            $errorCode = $errno;
157 6
        });
158
159 6
        $content = file_get_contents($path);
160
161 6
        restore_error_handler();
162
163 6 View Code Duplication
        if (null !== $errorMessage) {
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...
164 1
            if (false !== $pos = strpos($errorMessage, '): ')) {
165
                // cut "file_get_contents(%path%):" to make message more readable
166 1
                $errorMessage = substr($errorMessage, $pos + 3);
167
            }
168
169 1
            throw new IOException(sprintf(
170 1
                'Could not read %s: %s (%s)',
171
                $path,
172
                $errorMessage,
173
                $errorCode
174
            ), $errorCode);
175
        }
176
177
        try {
178 5
            return $this->decode($content, $schema);
179 4
        } catch (DecodingFailedException $e) {
180
            // Add the file name to the exception
181 1
            throw new DecodingFailedException(sprintf(
182 1
                'An error happened while decoding %s: %s',
183
                $path,
184 1
                $e->getMessage()
185 1
            ), $e->getCode(), $e);
186 3
        } catch (ValidationFailedException $e) {
187
            // Add the file name to the exception
188 2
            throw new ValidationFailedException(sprintf(
189 2
                "Validation of %s failed:\n%s",
190
                $path,
191 2
                $e->getErrorsAsString()
192 2
            ), $e->getErrors(), $e->getCode(), $e);
193 1
        } catch (InvalidSchemaException $e) {
194
            // Add the file name to the exception
195 1
            throw new InvalidSchemaException(sprintf(
196 1
                'An error happened while decoding %s: %s',
197
                $path,
198 1
                $e->getMessage()
199 1
            ), $e->getCode(), $e);
200
        }
201
    }
202
203
    /**
204
     * Returns the maximum recursion depth.
205
     *
206
     * A depth of zero means that objects are not allowed. A depth of one means
207
     * only one level of objects or arrays is allowed.
208
     *
209
     * @return int The maximum recursion depth
210
     */
211
    public function getMaxDepth()
212
    {
213
        return $this->maxDepth;
214
    }
215
216
    /**
217
     * Sets the maximum recursion depth.
218
     *
219
     * If the depth is exceeded during decoding, an {@link DecodingnFailedException}
220
     * will be thrown.
221
     *
222
     * A depth of zero means that objects are not allowed. A depth of one means
223
     * only one level of objects or arrays is allowed.
224
     *
225
     * @param int $maxDepth The maximum recursion depth
226
     *
227
     * @throws \InvalidArgumentException If the depth is not an integer greater
228
     *                                   than or equal to zero
229
     */
230 6 View Code Duplication
    public function setMaxDepth($maxDepth)
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...
231
    {
232 6
        if (!is_int($maxDepth)) {
233 1
            throw new \InvalidArgumentException(sprintf(
234 1
                'The maximum depth should be an integer. Got: %s',
235 1
                is_object($maxDepth) ? get_class($maxDepth) : gettype($maxDepth)
236
            ));
237
        }
238
239 5
        if ($maxDepth < 1) {
240 1
            throw new \InvalidArgumentException(sprintf(
241 1
                'The maximum depth should 1 or greater. Got: %s',
242
                $maxDepth
243
            ));
244
        }
245
246 4
        $this->maxDepth = $maxDepth;
247 4
    }
248
249
    /**
250
     * Returns the decoding of JSON objects.
251
     *
252
     * @return int One of the constants {@link JSON_OBJECT} and {@link ASSOC_ARRAY}
253
     */
254
    public function getObjectDecoding()
255
    {
256
        return $this->objectDecoding;
257
    }
258
259
    /**
260
     * Sets the decoding of JSON objects.
261
     *
262
     * By default, JSON objects are decoded as instances of {@link \stdClass}.
263
     *
264
     * @param int $decoding One of the constants {@link JSON_OBJECT} and {@link ASSOC_ARRAY}
265
     *
266
     * @throws \InvalidArgumentException If the passed decoding is invalid
267
     */
268 7 View Code Duplication
    public function setObjectDecoding($decoding)
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...
269
    {
270 7
        if (self::OBJECT !== $decoding && self::ASSOC_ARRAY !== $decoding) {
271 3
            throw new \InvalidArgumentException(sprintf(
272
                'Expected JsonDecoder::JSON_OBJECT or JsonDecoder::ASSOC_ARRAY. '.
273 3
                'Got: %s',
274
                $decoding
275
            ));
276
        }
277
278 4
        $this->objectDecoding = $decoding;
279 4
    }
280
281
    /**
282
     * Returns the decoding of big integers.
283
     *
284
     * @return int One of the constants {@link FLOAT} and {@link JSON_STRING}
285
     */
286
    public function getBigIntDecoding()
287
    {
288
        return $this->bigIntDecoding;
289
    }
290
291
    /**
292
     * Sets the decoding of big integers.
293
     *
294
     * By default, big integers are decoded as floats.
295
     *
296
     * @param int $decoding One of the constants {@link FLOAT} and {@link JSON_STRING}
297
     *
298
     * @throws \InvalidArgumentException If the passed decoding is invalid
299
     */
300 5 View Code Duplication
    public function setBigIntDecoding($decoding)
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...
301
    {
302 5
        if (self::FLOAT !== $decoding && self::STRING !== $decoding) {
303 3
            throw new \InvalidArgumentException(sprintf(
304
                'Expected JsonDecoder::FLOAT or JsonDecoder::JSON_STRING. '.
305 3
                'Got: %s',
306
                $decoding
307
            ));
308
        }
309
310 2
        $this->bigIntDecoding = $decoding;
311 2
    }
312
313 25
    private function decodeJson($json)
314
    {
315 25
        $assoc = self::ASSOC_ARRAY === $this->objectDecoding;
316
317 25
        if (PHP_VERSION_ID >= 50400 && !defined('JSON_C_VERSION')) {
318 25
            $options = self::STRING === $this->bigIntDecoding ? JSON_BIGINT_AS_STRING : 0;
319
320 25
            $decoded = json_decode($json, $assoc, $this->maxDepth, $options);
321
        } else {
322
            $decoded = json_decode($json, $assoc, $this->maxDepth);
323
        }
324
325
        // Data could not be decoded
326 25
        if (null === $decoded && 'null' !== $json) {
327 4
            $parser = new JsonParser();
328 4
            $e = $parser->lint($json);
329
330 4
            if ($e instanceof ParsingException) {
331
                throw new DecodingFailedException(sprintf(
332
                    'The JSON data could not be decoded: %s.',
333
                    $e->getMessage()
334
                ), 0, $e);
335
            }
336
337
            // $e is null if json_decode() failed, but the linter did not find
338
            // any problems. Happens for example when the max depth is exceeded.
339 4
            throw new DecodingFailedException(sprintf(
340 4
                'The JSON data could not be decoded: %s.',
341 4
                JsonError::getLastErrorMessage()
342
            ), json_last_error());
343
        }
344
345 21
        return $decoded;
346
    }
347
}
348