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 (#31)
by
unknown
01:47
created

JsonSerializer::unserializeData()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
c 0
b 0
f 0
rs 8.8571
cc 5
eloc 8
nc 4
nop 1
1
<?php
2
3
namespace Zumba\JsonSerializer;
4
5
use ReflectionClass;
6
use ReflectionException;
7
use SplObjectStorage;
8
use Zumba\Contracts\EntitySerializer;
9
use Zumba\JsonSerializer\EntitySerializers\ClosureEntitySerializer;
10
use Zumba\JsonSerializer\EntitySerializers\DateTimeEntitySerializer;
11
use Zumba\JsonSerializer\EntitySerializers\SplDoublyLinkedListEntitySerializer;
12
use Zumba\JsonSerializer\Exception\JsonSerializerException;
13
14
class JsonSerializer
15
{
16
    const CLASS_IDENTIFIER_KEY = '@type';
17
    const CLOSURE_IDENTIFIER_KEY = '@closure';
18
    const UTF8ENCODED_IDENTIFIER_KEY = '@utf8encoded';
19
    const SCALAR_IDENTIFIER_KEY = '@scalar';
20
    const FLOAT_ADAPTER = 'JsonSerializerFloatAdapter';
21
22
    const KEY_UTF8ENCODED = 1;
23
    const VALUE_UTF8ENCODED = 2;
24
25
    /**
26
     * Storage for object
27
     *
28
     * Used for recursion
29
     *
30
     * @var SplObjectStorage
31
     */
32
    protected $objectStorage;
33
34
    /**
35
     * Object mapping for recursion
36
     *
37
     * @var array
38
     */
39
    protected $objectMapping = [];
40
41
    /**
42
     * Object mapping index
43
     *
44
     * @var integer
45
     */
46
    protected $objectMappingIndex = 0;
47
48
    /**
49
     * Support PRESERVE_ZERO_FRACTION json option
50
     *
51
     * @var boolean
52
     */
53
    protected $preserveZeroFractionSupport;
54
55
    /**
56
     * Map of custom object serializers
57
     *
58
     * @var array
59
     */
60
    protected $customObjectSerializerMap;
61
62
    /**
63
     * JsonSerializer constructor.
64
     * @param ClosureSerializerInterface|null $closureSerializer
65
     */
66
    public function __construct(ClosureSerializerInterface $closureSerializer = null)
67
    {
68
        $this->preserveZeroFractionSupport = defined('JSON_PRESERVE_ZERO_FRACTION');
69
        $this->registerEntitySerializer(new DateTimeEntitySerializer());
70
        $this->registerEntitySerializer(new SplDoublyLinkedListEntitySerializer());
71
        if ($closureSerializer != null) {
72
            $this->registerEntitySerializer(new ClosureEntitySerializer($closureSerializer));
73
        }
74
75
    }
76
77
    /**
78
     * @param $serializer
79
     * @return JsonSerializer
80
     */
81
    public function registerEntitySerializer(EntitySerializer $serializer)
82
    {
83
        $this->customObjectSerializerMap[$serializer->getType()] = $serializer;
84
85
        return $this;
86
    }
87
88
    /**
89
     * @param $entity
90
     * @return EntitySerializer|null
91
     */
92
    protected function resolveEntitySerializer($entity)
93
    {
94
        if (($serialiser = $this->resolveEntitySerializerByDirectClassName($entity)) != null) {
95
            return $serialiser;
96
        }
97
98
        if (($serialiser = $this->resolveEntitySerializerByDirectClassName($entity)) != null) {
99
            return $serialiser;
100
        }
101
    }
102
103
    /**
104
     * @param $className
105
     * @return EntitySerializer|null
106
     */
107
    protected function resolveEntitySerializerByDirectClassName($className)
108
    {
109
        if (is_object($className)) {
110
            $className = get_class($className);
111
        }
112
113
        if (is_string($className) && class_exists($className)) {
114
            foreach ($this->customObjectSerializerMap as $type => $serialiser) {
115
                if ($type == $className) {
116
                    return $serialiser;
117
                }
118
            }
119
        }
120
    }
121
122
    /**
123
     * @param $object
124
     * @return EntitySerializer|null
125
     */
126
    protected function resolveEntitySerializerByInheritance($object)
127
    {
128
        if (is_string($object) && class_exists($object)) {
129
            list($ref, $object) = $this->getObjectInstance($object);
0 ignored issues
show
Unused Code introduced by
The assignment to $ref is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
130
        }
131
132
        foreach ($this->customObjectSerializerMap as $type => $serialiser) {
133
            if ($object instanceof $type) {
134
                return $serialiser;
135
            }
136
        }
137
    }
138
139
    /**
140
     * Serialize the value in JSON
141
     *
142
     * @param mixed $value
143
     * @return string JSON encoded
144
     * @throws JsonSerializerException
145
     */
146
    public function serialize($value)
147
    {
148
        $this->reset();
149
        $serializedData = $this->serializeData($value);
150
        $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
151
        if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
152
            if (json_last_error() != JSON_ERROR_UTF8) {
153
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
154
            }
155
156
            $serializedData = $this->encodeNonUtf8ToUtf8($serializedData);
157
            $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
158
159
            if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
160
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
161
            }
162
        }
163
        return $this->processEncodedValue($encoded);
164
    }
165
166
    /**
167
     * Calculate encoding options
168
     *
169
     * @return integer
170
     */
171
    protected function calculateEncodeOptions()
172
    {
173
        $options = JSON_UNESCAPED_UNICODE;
174
        if ($this->preserveZeroFractionSupport) {
175
            $options |= JSON_PRESERVE_ZERO_FRACTION;
176
        }
177
        return $options;
178
    }
179
180
    /**
181
     * @param mixed $serializedData
182
     *
183
     * @return array
184
     */
185
    protected function encodeNonUtf8ToUtf8($serializedData)
186
    {
187
        if (is_string($serializedData)) {
188
            if (!mb_check_encoding($serializedData, 'UTF-8')) {
189
                $serializedData = [
190
                    static::SCALAR_IDENTIFIER_KEY      => mb_convert_encoding($serializedData, 'UTF-8', '8bit'),
191
                    static::UTF8ENCODED_IDENTIFIER_KEY => static::VALUE_UTF8ENCODED,
192
                ];
193
            }
194
195
            return $serializedData;
196
        }
197
198
        $encodedKeys = [];
199
        $encodedData = [];
200
        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...
201
            if (is_array($value)) {
202
                $value = $this->encodeNonUtf8ToUtf8($value);
203
            }
204
205
            if (!mb_check_encoding($key, 'UTF-8')) {
206
                $key = mb_convert_encoding($key, 'UTF-8', '8bit');
207
                $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::KEY_UTF8ENCODED;
208
            }
209
210
            if (is_string($value)) {
211
                if (!mb_check_encoding($value, 'UTF-8')) {
212
                    $value = mb_convert_encoding($value, 'UTF-8', '8bit');
213
                    $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::VALUE_UTF8ENCODED;
214
                }
215
            }
216
217
            $encodedData[$key] = $value;
218
        }
219
220
        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...
221
            $encodedData[self::UTF8ENCODED_IDENTIFIER_KEY] = $encodedKeys;
222
        }
223
224
        return $encodedData;
225
    }
226
227
    /**
228
     * Execute post-encoding actions
229
     *
230
     * @param string $encoded
231
     * @return string
232
     */
233
    protected function processEncodedValue($encoded)
234
    {
235
        if (!$this->preserveZeroFractionSupport) {
236
            $encoded = preg_replace('/"' . static::FLOAT_ADAPTER . '\((.*?)\)"/', '\1', $encoded);
237
        }
238
        return $encoded;
239
    }
240
241
242
    /**
243
     * Parse the data to be json encoded
244
     *
245
     * @param mixed $value
246
     * @return mixed
247
     * @throws JsonSerializerException
248
     */
249
    protected function serializeData($value)
250
    {
251
        if (is_scalar($value) || $value === null) {
252
            if (!$this->preserveZeroFractionSupport && is_float($value) && ctype_digit((string)$value)) {
253
                // Because the PHP bug #50224, the float numbers with no
254
                // precision numbers are converted to integers when encoded
255
                $value = static::FLOAT_ADAPTER . '(' . $value . '.0)';
256
            }
257
            return $value;
258
        } elseif (is_resource($value)) {
259
            throw new JsonSerializerException('Resource is not supported in JsonSerializer');
260
        } elseif (is_array($value)) {
261
            return array_map([$this, __FUNCTION__], $value);
262
        } elseif ($value instanceof \Closure) {
263
            return $this->serializeClosure($value);
264
        }
265
        return $this->serializeObject($value);
266
267
    }
268
269
    /**
270
     * Extract the data from an object
271
     *
272
     * @param object $value
273
     * @return array
274
     */
275
    protected function serializeObject($value)
276
    {
277
        if ($this->objectStorage->contains($value)) {
278
            return [static::CLASS_IDENTIFIER_KEY => '@' . $this->objectStorage[$value]];
279
        }
280
281
        $this->objectStorage->attach($value, $this->objectMappingIndex++);
282
        $ref = new ReflectionClass($value);
283
        $className = $ref->getName();
284
285
        if (($serializer = $this->resolveEntitySerializer($value)) != null) {
286
            $data = [static::CLASS_IDENTIFIER_KEY => $className];
287
            $data += $serializer->serialize($value);
288
            return $data;
289
        }
290
291
        $paramsToSerialize = $this->getObjectProperties($ref, $value);
292
        $data = [static::CLASS_IDENTIFIER_KEY => $className];
293
        $data += array_map([$this, 'serializeData'], $this->extractObjectData($value, $ref, $paramsToSerialize));
294
        return $data;
295
    }
296
297
    /**
298
     * Return the list of properties to be serialized
299
     *
300
     * @param ReflectionClass $ref
301
     * @param object $value
302
     * @return array
303
     */
304
    protected function getObjectProperties($ref, $value)
305
    {
306
        if (method_exists($value, '__sleep')) {
307
            return $value->__sleep();
308
        }
309
310
        $props = [];
311
        foreach ($ref->getProperties() as $prop) {
312
            $props[] = $prop->getName();
313
        }
314
        return array_unique(array_merge($props, array_keys(get_object_vars($value))));
315
    }
316
317
    /**
318
     * Extract the object data
319
     *
320
     * @param object $value
321
     * @param ReflectionClass $ref
322
     * @param array $properties
323
     * @return array
324
     */
325
    protected function extractObjectData($value, $ref, $properties)
326
    {
327
        $data = [];
328
        foreach ($properties as $property) {
329
            try {
330
                $propRef = $ref->getProperty($property);
331
                $propRef->setAccessible(true);
332
                $data[$property] = $propRef->getValue($value);
333
            } catch (ReflectionException $e) {
334
                $data[$property] = $value->$property;
335
            }
336
        }
337
        return $data;
338
    }
339
340
    /**
341
     * Unserialize the value from JSON
342
     *
343
     * @param string $value
344
     * @return mixed
345
     */
346
    public function unserialize($value)
347
    {
348
        $this->reset();
349
        $data = json_decode($value, true);
350
        if ($data === null && json_last_error() != JSON_ERROR_NONE) {
351
            throw new JsonSerializerException('Invalid JSON to unserialize.');
352
        }
353
354
        if (mb_strpos($value, static::UTF8ENCODED_IDENTIFIER_KEY) !== false) {
355
            $data = $this->decodeNonUtf8FromUtf8($data);
356
        }
357
358
        return $this->unserializeData($data);
359
    }
360
361
    /**
362
     * Parse the json decode to convert to objects again
363
     *
364
     * @param mixed $value
365
     * @return mixed
366
     */
367
    protected function unserializeData($value)
368
    {
369
        if (is_scalar($value) || $value === null) {
370
            return $value;
371
        } elseif (isset($value[static::CLASS_IDENTIFIER_KEY])) {
372
            return $this->unserializeObject($value);
0 ignored issues
show
Bug introduced by
It seems like $value defined by parameter $value on line 367 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...
373
        } elseif (!empty($value[static::CLOSURE_IDENTIFIER_KEY])) {
374
            return $this->unserializeClosure($value);
375
        }
376
377
        return array_map([$this, __FUNCTION__], $value);
378
    }
379
380
    /**
381
     * @param mixed $serializedData
382
     *
383
     * @return mixed
384
     */
385
    protected function decodeNonUtf8FromUtf8($serializedData)
386
    {
387
        if (is_array($serializedData) && isset($serializedData[static::SCALAR_IDENTIFIER_KEY])) {
388
            $serializedData = mb_convert_encoding($serializedData[static::SCALAR_IDENTIFIER_KEY], '8bit', 'UTF-8');
389
            return $serializedData;
390
        } elseif (is_scalar($serializedData) || $serializedData === null) {
391
            return $serializedData;
392
        }
393
394
        $encodedKeys = [];
395
        if (isset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY])) {
396
            $encodedKeys = $serializedData[static::UTF8ENCODED_IDENTIFIER_KEY];
397
            unset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY]);
398
        }
399
400
        $decodedData = [];
401
        foreach ($serializedData as $key => $value) {
402
            if (is_array($value)) {
403
                $value = $this->decodeNonUtf8FromUtf8($value);
404
            }
405
406
            if (isset($encodedKeys[$key])) {
407
                $originalKey = $key;
408
                if ($encodedKeys[$key] & static::KEY_UTF8ENCODED) {
409
                    $key = mb_convert_encoding($key, '8bit', 'UTF-8');
410
                }
411
                if ($encodedKeys[$originalKey] & static::VALUE_UTF8ENCODED) {
412
                    $value = mb_convert_encoding($value, '8bit', 'UTF-8');
413
                }
414
            }
415
416
            $decodedData[$key] = $value;
417
        }
418
419
        return $decodedData;
420
    }
421
422
    /**
423
     * Convert the serialized array into an object
424
     *
425
     * @param array $value
426
     * @return object
427
     * @throws JsonSerializerException
428
     */
429
    protected function unserializeObject($value)
430
    {
431
        $className = $value[static::CLASS_IDENTIFIER_KEY];
432
        unset($value[static::CLASS_IDENTIFIER_KEY]);
433
434
        if ($className[0] === '@') {
435
            $index = substr($className, 1);
436
            return $this->objectMapping[$index];
437
        } elseif (!class_exists($className)) {
438
            throw new JsonSerializerException('Unable to find class ' . $className);
439
        } elseif (($serialier = $this->resolveEntitySerializer($className)) != null) {
440
            $obj = $serialier->unserialize($value);
441
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
442
            return $obj;
443
        }
444
445
        list($ref, $object) = $this->getObjectInstance($className);
446
        $this->objectMapping[$this->objectMappingIndex++] = $object;
447
        foreach ($value as $property => $propertyValue) {
448
            try {
449
                $propRef = $ref->getProperty($property);
450
                $propRef->setAccessible(true);
451
                $propRef->setValue($object, $this->unserializeData($propertyValue));
452
            } catch (ReflectionException $e) {
453
                $object->$property = $this->unserializeData($propertyValue);
454
            }
455
        }
456
457
        if (method_exists($object, '__wakeup')) {
458
            $object->__wakeup();
459
        }
460
        return $object;
461
    }
462
463
    /**
464
     * @param $className
465
     * @return object
466
     */
467
    protected function getObjectInstance($className)
468
    {
469
        $ref = new ReflectionClass($className);
470
        return [$ref, $ref->newInstanceWithoutConstructor()];
471
    }
472
473
    /**
474
     * @param $value
475
     * @return array
476
     */
477
    protected function serializeClosure($value)
478
    {
479
        if (($serializer = $this->resolveEntitySerializer(\Closure::class)) == null) {
480
            throw new JsonSerializerException('Closure serializer not provided to unserialize closure');
481
        }
482
        return $serializer->serialize($value);
483
    }
484
485
    /**
486
     * @param $value
487
     * @return mixed
488
     */
489
    protected function unserializeClosure($value)
490
    {
491
        if (($serializer = $this->resolveEntitySerializer(\Closure::class)) == null) {
492
            throw new JsonSerializerException('Closure serializer not provided to unserialize closure');
493
        }
494
        return $serializer->unserialize($value);
495
    }
496
497
    /**
498
     * Reset variables
499
     *
500
     * @return void
501
     */
502
    protected function reset()
503
    {
504
        $this->objectStorage = new SplObjectStorage();
505
        $this->objectMapping = [];
506
        $this->objectMappingIndex = 0;
507
    }
508
}
509