GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#29)
by
unknown
01:47
created

JsonSerializer::isSplList()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace Zumba\JsonSerializer;
4
5
use ReflectionClass;
6
use ReflectionException;
7
use SplObjectStorage;
8
use Zumba\JsonSerializer\Exception\JsonSerializerException;
9
use SuperClosure\SerializerInterface as ClosureSerializerInterface;
10
11
class JsonSerializer
12
{
13
14
    const CLASS_IDENTIFIER_KEY = '@type';
15
    const CLOSURE_IDENTIFIER_KEY = '@closure';
16
    const UTF8ENCODED_IDENTIFIER_KEY = '@utf8encoded';
17
    const SCALAR_IDENTIFIER_KEY = '@scalar';
18
    const FLOAT_ADAPTER = 'JsonSerializerFloatAdapter';
19
20
    const KEY_UTF8ENCODED = 1;
21
    const VALUE_UTF8ENCODED = 2;
22
23
    /**
24
     * Storage for object
25
     *
26
     * Used for recursion
27
     *
28
     * @var SplObjectStorage
29
     */
30
    protected $objectStorage;
31
32
    /**
33
     * Object mapping for recursion
34
     *
35
     * @var array
36
     */
37
    protected $objectMapping = array();
38
39
    /**
40
     * Object mapping index
41
     *
42
     * @var integer
43
     */
44
    protected $objectMappingIndex = 0;
45
46
    /**
47
     * Support PRESERVE_ZERO_FRACTION json option
48
     *
49
     * @var boolean
50
     */
51
    protected $preserveZeroFractionSupport;
52
53
    /**
54
     * Closure serializer instance
55
     *
56
     * @var ClosureSerializerInterface
57
     */
58
    protected $closureSerializer;
59
60
    /**
61
     * Map of custom object serializers
62
     *
63
     * @var array
64
     */
65
    protected $customObjectSerializerMap;
66
67
    /**
68
     * Constructor.
69
     *
70
     * @param ClosureSerializerInterface $closureSerializer
71
     * @param array $customObjectSerializerMap
72
     */
73
    public function __construct(ClosureSerializerInterface $closureSerializer = null, $customObjectSerializerMap = array())
74
    {
75
        $this->preserveZeroFractionSupport = defined('JSON_PRESERVE_ZERO_FRACTION');
76
        $this->closureSerializer = $closureSerializer;
77
        $this->customObjectSerializerMap = (array)$customObjectSerializerMap;
78
    }
79
80
    /**
81
     * Serialize the value in JSON
82
     *
83
     * @param mixed $value
84
     * @return string JSON encoded
85
     * @throws JsonSerializerException
86
     */
87
    public function serialize($value)
88
    {
89
        $this->reset();
90
        $serializedData = $this->serializeData($value);
91
        $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
92
        if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
93
            if (json_last_error() != JSON_ERROR_UTF8) {
94
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
95
            }
96
97
            $serializedData = $this->encodeNonUtf8ToUtf8($serializedData);
98
            $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
99
100
            if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
101
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
102
            }
103
        }
104
        return $this->processEncodedValue($encoded);
105
    }
106
107
    /**
108
     * Calculate encoding options
109
     *
110
     * @return integer
111
     */
112
    protected function calculateEncodeOptions()
113
    {
114
        $options = JSON_UNESCAPED_UNICODE;
115
        if ($this->preserveZeroFractionSupport) {
116
            $options |= JSON_PRESERVE_ZERO_FRACTION;
117
        }
118
        return $options;
119
    }
120
121
    /**
122
     * @param mixed $serializedData
123
     *
124
     * @return array
125
     */
126
    protected function encodeNonUtf8ToUtf8($serializedData)
127
    {
128
        if (is_string($serializedData)) {
129
            if (!mb_check_encoding($serializedData, 'UTF-8')) {
130
                $serializedData = [
131
                    static::SCALAR_IDENTIFIER_KEY => mb_convert_encoding($serializedData, 'UTF-8', '8bit'),
132
                    static::UTF8ENCODED_IDENTIFIER_KEY => static::VALUE_UTF8ENCODED,
133
                ];
134
            }
135
136
            return $serializedData;
137
        }
138
139
        $encodedKeys = [];
140
        $encodedData = [];
141
        foreach ($serializedData as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $serializedData of type object|integer|double|null|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
142
            if (is_array($value)) {
143
                $value = $this->encodeNonUtf8ToUtf8($value);
144
            }
145
146
            if (!mb_check_encoding($key, 'UTF-8')) {
147
                $key = mb_convert_encoding($key, 'UTF-8', '8bit');
148
                $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::KEY_UTF8ENCODED;
149
            }
150
151
            if (is_string($value)) {
152
                if (!mb_check_encoding($value, 'UTF-8')) {
153
                    $value = mb_convert_encoding($value, 'UTF-8', '8bit');
154
                    $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::VALUE_UTF8ENCODED;
155
                }
156
            }
157
158
            $encodedData[$key] = $value;
159
        }
160
161
        if ($encodedKeys) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $encodedKeys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
162
            $encodedData[self::UTF8ENCODED_IDENTIFIER_KEY] = $encodedKeys;
163
        }
164
165
        return $encodedData;
166
    }
167
168
    /**
169
     * Execute post-encoding actions
170
     *
171
     * @param string $encoded
172
     * @return string
173
     */
174
    protected function processEncodedValue($encoded)
175
    {
176
        if (!$this->preserveZeroFractionSupport) {
177
            $encoded = preg_replace('/"' . static::FLOAT_ADAPTER . '\((.*?)\)"/', '\1', $encoded);
178
        }
179
        return $encoded;
180
    }
181
182
    /**
183
     * Unserialize the value from JSON
184
     *
185
     * @param string $value
186
     * @return mixed
187
     */
188
    public function unserialize($value)
189
    {
190
        $this->reset();
191
        $data = json_decode($value, true);
192
        if ($data === null && json_last_error() != JSON_ERROR_NONE) {
193
            throw new JsonSerializerException('Invalid JSON to unserialize.');
194
        }
195
196
        if (mb_strpos($value, static::UTF8ENCODED_IDENTIFIER_KEY) !== false) {
197
            $data = $this->decodeNonUtf8FromUtf8($data);
198
        }
199
200
        return $this->unserializeData($data);
201
    }
202
203
    /**
204
     * Parse the data to be json encoded
205
     *
206
     * @param mixed $value
207
     * @return mixed
208
     * @throws JsonSerializerException
209
     */
210
    protected function serializeData($value)
211
    {
212
        if (is_scalar($value) || $value === null) {
213
            if (!$this->preserveZeroFractionSupport && is_float($value) && ctype_digit((string)$value)) {
214
                // Because the PHP bug #50224, the float numbers with no
215
                // precision numbers are converted to integers when encoded
216
                $value = static::FLOAT_ADAPTER . '(' . $value . '.0)';
217
            }
218
            return $value;
219
        }
220
        if (is_resource($value)) {
221
            throw new JsonSerializerException('Resource is not supported in JsonSerializer');
222
        }
223
        if (is_array($value)) {
224
            return array_map(array($this, __FUNCTION__), $value);
225
        }
226
        if ($value instanceof \Closure) {
227
            if (!$this->closureSerializer) {
228
                throw new JsonSerializerException('Closure serializer not given. Unable to serialize closure.');
229
            }
230
            return array(
231
                static::CLOSURE_IDENTIFIER_KEY => true,
232
                'value' => $this->closureSerializer->serialize($value)
233
            );
234
        }
235
        return $this->serializeObject($value);
236
    }
237
238
    /**
239
     * Extract the data from an object
240
     *
241
     * @param object $value
242
     * @return array
243
     */
244
    protected function serializeObject($value)
245
    {
246
        if ($this->objectStorage->contains($value)) {
247
            return array(static::CLASS_IDENTIFIER_KEY => '@' . $this->objectStorage[$value]);
248
        }
249
        $this->objectStorage->attach($value, $this->objectMappingIndex++);
250
251
        $ref = new ReflectionClass($value);
252
        $className = $ref->getName();
253
        if (array_key_exists($className, $this->customObjectSerializerMap)) {
254
            $data = array(static::CLASS_IDENTIFIER_KEY => $className);
255
            $data += $this->customObjectSerializerMap[$className]->serialize($value);
256
            return $data;
257
        }
258
259
        $paramsToSerialize = $this->getObjectProperties($ref, $value);
260
        $data = array(static::CLASS_IDENTIFIER_KEY => $className);
261
262
        if($value instanceof \SplDoublyLinkedList){
263
            return $data + array('value' => $value->serialize());
264
        }
265
266
        $data += array_map(array($this, 'serializeData'), $this->extractObjectData($value, $ref, $paramsToSerialize));
267
        return $data;
268
    }
269
270
    /**
271
     * Return the list of properties to be serialized
272
     *
273
     * @param ReflectionClass $ref
274
     * @param object $value
275
     * @return array
276
     */
277
    protected function getObjectProperties($ref, $value)
278
    {
279
        if (method_exists($value, '__sleep')) {
280
            return $value->__sleep();
281
        }
282
283
        $props = array();
284
        foreach ($ref->getProperties() as $prop) {
285
            $props[] = $prop->getName();
286
        }
287
        return array_unique(array_merge($props, array_keys(get_object_vars($value))));
288
    }
289
290
    /**
291
     * Extract the object data
292
     *
293
     * @param object $value
294
     * @param ReflectionClass $ref
295
     * @param array $properties
296
     * @return array
297
     */
298
    protected function extractObjectData($value, $ref, $properties)
299
    {
300
        $data = array();
301
        foreach ($properties as $property) {
302
            try {
303
                $propRef = $ref->getProperty($property);
304
                $propRef->setAccessible(true);
305
                $data[$property] = $propRef->getValue($value);
306
            } catch (ReflectionException $e) {
307
                $data[$property] = $value->$property;
308
            }
309
        }
310
        return $data;
311
    }
312
313
    /**
314
     * Parse the json decode to convert to objects again
315
     *
316
     * @param mixed $value
317
     * @return mixed
318
     */
319
    protected function unserializeData($value)
320
    {
321
        if (is_scalar($value) || $value === null) {
322
            return $value;
323
        }
324
325
        if (isset($value[static::CLASS_IDENTIFIER_KEY])) {
326
            return $this->unserializeObject($value);
0 ignored issues
show
Bug introduced by
It seems like $value defined by parameter $value on line 319 can also be of type object; however, Zumba\JsonSerializer\Jso...er::unserializeObject() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
327
        }
328
329
        if (!empty($value[static::CLOSURE_IDENTIFIER_KEY])) {
330
            if (!$this->closureSerializer) {
331
                throw new JsonSerializerException('Closure serializer not provided to unserialize closure');
332
            }
333
            return $this->closureSerializer->unserialize($value['value']);
334
        }
335
336
        return array_map(array($this, __FUNCTION__), $value);
337
    }
338
339
    /**
340
     * @param mixed $serializedData
341
     *
342
     * @return mixed
343
     */
344
    protected function decodeNonUtf8FromUtf8($serializedData)
345
    {
346
        if (is_array($serializedData) && isset($serializedData[static::SCALAR_IDENTIFIER_KEY])) {
347
            $serializedData = mb_convert_encoding($serializedData[static::SCALAR_IDENTIFIER_KEY], '8bit', 'UTF-8');
348
            return $serializedData;
349
        } elseif (is_scalar($serializedData) || $serializedData === null) {
350
            return $serializedData;
351
        }
352
353
        $encodedKeys = [];
354
        if (isset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY])) {
355
            $encodedKeys = $serializedData[static::UTF8ENCODED_IDENTIFIER_KEY];
356
            unset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY]);
357
        }
358
359
        $decodedData = [];
360
        foreach ($serializedData as $key => $value) {
361
            if (is_array($value)) {
362
                $value = $this->decodeNonUtf8FromUtf8($value);
363
            }
364
365
            if (isset($encodedKeys[$key])) {
366
                $originalKey = $key;
367
                if ($encodedKeys[$key] & static::KEY_UTF8ENCODED) {
368
                    $key = mb_convert_encoding($key, '8bit', 'UTF-8');
369
                }
370
                if ($encodedKeys[$originalKey] & static::VALUE_UTF8ENCODED) {
371
                    $value = mb_convert_encoding($value, '8bit', 'UTF-8');
372
                }
373
            }
374
375
            $decodedData[$key] = $value;
376
        }
377
378
        return $decodedData;
379
    }
380
381
    /**
382
     * Convert the serialized array into an object
383
     *
384
     * @param array $value
385
     * @return object
386
     * @throws JsonSerializerException
387
     */
388
    protected function unserializeObject($value)
389
    {
390
        $className = $value[static::CLASS_IDENTIFIER_KEY];
391
        unset($value[static::CLASS_IDENTIFIER_KEY]);
392
393
        if ($className[0] === '@') {
394
            $index = substr($className, 1);
395
            return $this->objectMapping[$index];
396
        }
397
398
        if (array_key_exists($className, $this->customObjectSerializerMap)) {
399
            $obj = $this->customObjectSerializerMap[$className]->unserialize($value);
400
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
401
            return $obj;
402
        }
403
404
        if (!class_exists($className)) {
405
            throw new JsonSerializerException('Unable to find class ' . $className);
406
        }
407
408
        if ($className === 'DateTime') {
409
            $obj = $this->restoreUsingUnserialize($className, $value);
410
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
411
            return $obj;
412
        }
413
414
        if (!$this->isSplList($className)) {
415
            $ref = new ReflectionClass($className);
416
            $obj = $ref->newInstanceWithoutConstructor();
417
        } else {
418
            $obj = new $className();
419
        }
420
421
        if ($obj instanceof \SplDoublyLinkedList ) {
422
            $obj->unserialize($value['value']);
423
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
424
            return $obj;
425
        }
426
427
        $this->objectMapping[$this->objectMappingIndex++] = $obj;
428
        foreach ($value as $property => $propertyValue) {
429
            try {
430
                $propRef = $ref->getProperty($property);
0 ignored issues
show
Bug introduced by
The variable $ref does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
431
                $propRef->setAccessible(true);
432
                $propRef->setValue($obj, $this->unserializeData($propertyValue));
433
            } catch (ReflectionException $e) {
434
                $obj->$property = $this->unserializeData($propertyValue);
435
            }
436
        }
437
        if (method_exists($obj, '__wakeup')) {
438
            $obj->__wakeup();
439
        }
440
        return $obj;
441
    }
442
443
    /**
444
     * @return boolean
445
     */
446
    protected function isSplList($className)
447
    {
448
        return in_array($className, array('SplQueue', 'SplDoublyLinkedList', 'SplStack'));
449
    }
450
451
    protected function restoreUsingUnserialize($className, $attributes)
452
    {
453
        $obj = (object)$attributes;
454
        $serialized = preg_replace('|^O:\d+:"\w+":|', 'O:' . strlen($className) . ':"' . $className . '":', serialize($obj));
455
        return unserialize($serialized);
456
    }
457
458
    /**
459
     * Reset variables
460
     *
461
     * @return void
462
     */
463
    protected function reset()
464
    {
465
        $this->objectStorage = new SplObjectStorage();
466
        $this->objectMapping = array();
467
        $this->objectMappingIndex = 0;
468
    }
469
}
470