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
03:24
created

resolveEntitySerializerByInheritance()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 12
Code Lines 6

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 6
nc 6
nop 1
1
<?php
2
3
namespace Zumba\JsonSerializer;
4
5
use ReflectionClass;
6
use ReflectionException;
7
use SplObjectStorage;
8
use SuperClosure\SerializerInterface as ClosureSerializerInterface;
9
use Zumba\Contracts\EntitySerializer;
10
use Zumba\JsonSerializer\EntitySerializers\ClosureEntitySerializer;
11
use Zumba\JsonSerializer\EntitySerializers\DateTimeEntitySerializer;
12
use Zumba\JsonSerializer\EntitySerializers\SplDoublyLinkedListEntitySerializer;
13
use Zumba\JsonSerializer\Exception\JsonSerializerException;
14
15
class JsonSerializer
16
{
17
    const CLASS_IDENTIFIER_KEY = '@type';
18
    const CLOSURE_IDENTIFIER_KEY = '@closure';
19
    const UTF8ENCODED_IDENTIFIER_KEY = '@utf8encoded';
20
    const SCALAR_IDENTIFIER_KEY = '@scalar';
21
    const FLOAT_ADAPTER = 'JsonSerializerFloatAdapter';
22
23
    const KEY_UTF8ENCODED = 1;
24
    const VALUE_UTF8ENCODED = 2;
25
26
    /**
27
     * Storage for object
28
     *
29
     * Used for recursion
30
     *
31
     * @var SplObjectStorage
32
     */
33
    protected $objectStorage;
34
35
    /**
36
     * Object mapping for recursion
37
     *
38
     * @var array
39
     */
40
    protected $objectMapping = [];
41
42
    /**
43
     * Object mapping index
44
     *
45
     * @var integer
46
     */
47
    protected $objectMappingIndex = 0;
48
49
    /**
50
     * Support PRESERVE_ZERO_FRACTION json option
51
     *
52
     * @var boolean
53
     */
54
    protected $preserveZeroFractionSupport;
55
56
    /**
57
     * Map of custom object serializers
58
     *
59
     * @var array
60
     */
61
    protected $customObjectSerializerMap;
62
63
    /**
64
     * JsonSerializer constructor.
65
     * @param ClosureSerializerInterface|null $closureSerializer
66
     */
67
    public function __construct(ClosureSerializerInterface $closureSerializer = null)
68
    {
69
        $this->preserveZeroFractionSupport = defined('JSON_PRESERVE_ZERO_FRACTION');
70
        $this->registerEntitySerializer(new DateTimeEntitySerializer());
71
        $this->registerEntitySerializer(new SplDoublyLinkedListEntitySerializer());
72
        if ($closureSerializer != null) {
73
            $this->registerEntitySerializer(new ClosureEntitySerializer($closureSerializer));
74
        }
75
76
    }
77
78
    /**
79
     * @param $serializer
80
     * @return JsonSerializer
81
     */
82
    public function registerEntitySerializer(EntitySerializer $serializer)
83
    {
84
        $this->customObjectSerializerMap[$serializer->getType()] = $serializer;
85
86
        return $this;
87
    }
88
89
    /**
90
     * @param $entity
91
     * @return EntitySerializer|null
92
     */
93
    protected function resolveEntitySerializer($entity)
94
    {
95
        if (($serialiser = $this->resolveEntitySerializerByDirectClassName($entity)) != null) {
96
            return $serialiser;
97
        }
98
99
        if (($serialiser = $this->resolveEntitySerializerByDirectClassName($entity)) != null) {
100
            return $serialiser;
101
        }
102
    }
103
104
    /**
105
     * @param $className
106
     * @return EntitySerializer|null
107
     */
108
    protected function resolveEntitySerializerByDirectClassName($className)
109
    {
110
        if (is_object($className)) {
111
            $className = get_class($className);
112
        }
113
114
        if (is_string($className) && class_exists($className)) {
115
            foreach ($this->customObjectSerializerMap as $type => $serialiser) {
116
                if ($type == $className) {
117
                    return $serialiser;
118
                }
119
            }
120
        }
121
    }
122
123
    /**
124
     * @param $object
125
     * @return EntitySerializer|null
126
     */
127
    protected function resolveEntitySerializerByInheritance($object)
128
    {
129
        if (is_string($object) && class_exists($object)) {
130
            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...
131
        }
132
133
        foreach ($this->customObjectSerializerMap as $type => $serialiser) {
134
            if ($object instanceof $type) {
135
                return $serialiser;
136
            }
137
        }
138
    }
139
140
    /**
141
     * Serialize the value in JSON
142
     *
143
     * @param mixed $value
144
     * @return string JSON encoded
145
     * @throws JsonSerializerException
146
     */
147
    public function serialize($value)
148
    {
149
        $this->reset();
150
        $serializedData = $this->serializeData($value);
151
        $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
152
        if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
153
            if (json_last_error() != JSON_ERROR_UTF8) {
154
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
155
            }
156
157
            $serializedData = $this->encodeNonUtf8ToUtf8($serializedData);
158
            $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
159
160
            if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
161
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
162
            }
163
        }
164
        return $this->processEncodedValue($encoded);
165
    }
166
167
    /**
168
     * Calculate encoding options
169
     *
170
     * @return integer
171
     */
172
    protected function calculateEncodeOptions()
173
    {
174
        $options = JSON_UNESCAPED_UNICODE;
175
        if ($this->preserveZeroFractionSupport) {
176
            $options |= JSON_PRESERVE_ZERO_FRACTION;
177
        }
178
        return $options;
179
    }
180
181
    /**
182
     * @param mixed $serializedData
183
     *
184
     * @return array
185
     */
186
    protected function encodeNonUtf8ToUtf8($serializedData)
187
    {
188
        if (is_string($serializedData)) {
189
            if (!mb_check_encoding($serializedData, 'UTF-8')) {
190
                $serializedData = [
191
                    static::SCALAR_IDENTIFIER_KEY      => mb_convert_encoding($serializedData, 'UTF-8', '8bit'),
192
                    static::UTF8ENCODED_IDENTIFIER_KEY => static::VALUE_UTF8ENCODED,
193
                ];
194
            }
195
196
            return $serializedData;
197
        }
198
199
        $encodedKeys = [];
200
        $encodedData = [];
201
        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...
202
            if (is_array($value)) {
203
                $value = $this->encodeNonUtf8ToUtf8($value);
204
            }
205
206
            if (!mb_check_encoding($key, 'UTF-8')) {
207
                $key = mb_convert_encoding($key, 'UTF-8', '8bit');
208
                $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::KEY_UTF8ENCODED;
209
            }
210
211
            if (is_string($value)) {
212
                if (!mb_check_encoding($value, 'UTF-8')) {
213
                    $value = mb_convert_encoding($value, 'UTF-8', '8bit');
214
                    $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::VALUE_UTF8ENCODED;
215
                }
216
            }
217
218
            $encodedData[$key] = $value;
219
        }
220
221
        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...
222
            $encodedData[self::UTF8ENCODED_IDENTIFIER_KEY] = $encodedKeys;
223
        }
224
225
        return $encodedData;
226
    }
227
228
    /**
229
     * Execute post-encoding actions
230
     *
231
     * @param string $encoded
232
     * @return string
233
     */
234
    protected function processEncodedValue($encoded)
235
    {
236
        if (!$this->preserveZeroFractionSupport) {
237
            $encoded = preg_replace('/"' . static::FLOAT_ADAPTER . '\((.*?)\)"/', '\1', $encoded);
238
        }
239
        return $encoded;
240
    }
241
242
243
    /**
244
     * Parse the data to be json encoded
245
     *
246
     * @param mixed $value
247
     * @return mixed
248
     * @throws JsonSerializerException
249
     */
250
    protected function serializeData($value)
251
    {
252
        if (is_scalar($value) || $value === null) {
253
            if (!$this->preserveZeroFractionSupport && is_float($value) && ctype_digit((string)$value)) {
254
                // Because the PHP bug #50224, the float numbers with no
255
                // precision numbers are converted to integers when encoded
256
                $value = static::FLOAT_ADAPTER . '(' . $value . '.0)';
257
            }
258
            return $value;
259
        } elseif (is_resource($value)) {
260
            throw new JsonSerializerException('Resource is not supported in JsonSerializer');
261
        } elseif (is_array($value)) {
262
            return array_map([$this, __FUNCTION__], $value);
263
        } elseif ($value instanceof \Closure) {
264
            return $this->serializeClosure($value);
265
        }
266
        return $this->serializeObject($value);
267
268
    }
269
270
    /**
271
     * Extract the data from an object
272
     *
273
     * @param object $value
274
     * @return array
275
     */
276
    protected function serializeObject($value)
277
    {
278
        if ($this->objectStorage->contains($value)) {
279
            return [static::CLASS_IDENTIFIER_KEY => '@' . $this->objectStorage[$value]];
280
        }
281
282
        $this->objectStorage->attach($value, $this->objectMappingIndex++);
283
        $ref = new ReflectionClass($value);
284
        $className = $ref->getName();
285
286
        if (($serializer = $this->resolveEntitySerializer($value)) != null) {
287
            $data = [static::CLASS_IDENTIFIER_KEY => $className];
288
            $data += $serializer->serialize($value);
289
            return $data;
290
        }
291
292
        $paramsToSerialize = $this->getObjectProperties($ref, $value);
293
        $data = [static::CLASS_IDENTIFIER_KEY => $className];
294
        $data += array_map([$this, 'serializeData'], $this->extractObjectData($value, $ref, $paramsToSerialize));
295
        return $data;
296
    }
297
298
    /**
299
     * Return the list of properties to be serialized
300
     *
301
     * @param ReflectionClass $ref
302
     * @param object $value
303
     * @return array
304
     */
305
    protected function getObjectProperties($ref, $value)
306
    {
307
        if (method_exists($value, '__sleep')) {
308
            return $value->__sleep();
309
        }
310
311
        $props = [];
312
        foreach ($ref->getProperties() as $prop) {
313
            $props[] = $prop->getName();
314
        }
315
        return array_unique(array_merge($props, array_keys(get_object_vars($value))));
316
    }
317
318
    /**
319
     * Extract the object data
320
     *
321
     * @param object $value
322
     * @param ReflectionClass $ref
323
     * @param array $properties
324
     * @return array
325
     */
326
    protected function extractObjectData($value, $ref, $properties)
327
    {
328
        $data = [];
329
        foreach ($properties as $property) {
330
            try {
331
                $propRef = $ref->getProperty($property);
332
                $propRef->setAccessible(true);
333
                $data[$property] = $propRef->getValue($value);
334
            } catch (ReflectionException $e) {
335
                $data[$property] = $value->$property;
336
            }
337
        }
338
        return $data;
339
    }
340
341
    /**
342
     * Unserialize the value from JSON
343
     *
344
     * @param string $value
345
     * @return mixed
346
     */
347
    public function unserialize($value)
348
    {
349
        $this->reset();
350
        $data = json_decode($value, true);
351
        if ($data === null && json_last_error() != JSON_ERROR_NONE) {
352
            throw new JsonSerializerException('Invalid JSON to unserialize.');
353
        }
354
355
        if (mb_strpos($value, static::UTF8ENCODED_IDENTIFIER_KEY) !== false) {
356
            $data = $this->decodeNonUtf8FromUtf8($data);
357
        }
358
359
        return $this->unserializeData($data);
360
    }
361
362
    /**
363
     * Parse the json decode to convert to objects again
364
     *
365
     * @param mixed $value
366
     * @return mixed
367
     */
368
    protected function unserializeData($value)
369
    {
370
        if (is_scalar($value) || $value === null) {
371
            return $value;
372
        } elseif (isset($value[static::CLASS_IDENTIFIER_KEY])) {
373
            return $this->unserializeObject($value);
0 ignored issues
show
Bug introduced by
It seems like $value defined by parameter $value on line 368 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...
374
        } elseif (!empty($value[self::CLOSURE_IDENTIFIER_KEY])) {
375
            return $this->unserializeClosure($value);
376
        }
377
378
        return array_map([$this, __FUNCTION__], $value);
379
    }
380
381
    /**
382
     * @param mixed $serializedData
383
     *
384
     * @return mixed
385
     */
386
    protected function decodeNonUtf8FromUtf8($serializedData)
387
    {
388
        if (is_array($serializedData) && isset($serializedData[static::SCALAR_IDENTIFIER_KEY])) {
389
            $serializedData = mb_convert_encoding($serializedData[static::SCALAR_IDENTIFIER_KEY], '8bit', 'UTF-8');
390
            return $serializedData;
391
        } elseif (is_scalar($serializedData) || $serializedData === null) {
392
            return $serializedData;
393
        }
394
395
        $encodedKeys = [];
396
        if (isset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY])) {
397
            $encodedKeys = $serializedData[static::UTF8ENCODED_IDENTIFIER_KEY];
398
            unset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY]);
399
        }
400
401
        $decodedData = [];
402
        foreach ($serializedData as $key => $value) {
403
            if (is_array($value)) {
404
                $value = $this->decodeNonUtf8FromUtf8($value);
405
            }
406
407
            if (isset($encodedKeys[$key])) {
408
                $originalKey = $key;
409
                if ($encodedKeys[$key] & static::KEY_UTF8ENCODED) {
410
                    $key = mb_convert_encoding($key, '8bit', 'UTF-8');
411
                }
412
                if ($encodedKeys[$originalKey] & static::VALUE_UTF8ENCODED) {
413
                    $value = mb_convert_encoding($value, '8bit', 'UTF-8');
414
                }
415
            }
416
417
            $decodedData[$key] = $value;
418
        }
419
420
        return $decodedData;
421
    }
422
423
    /**
424
     * Convert the serialized array into an object
425
     *
426
     * @param array $value
427
     * @return object
428
     * @throws JsonSerializerException
429
     */
430
    protected function unserializeObject($value)
431
    {
432
        $className = $value[static::CLASS_IDENTIFIER_KEY];
433
        unset($value[static::CLASS_IDENTIFIER_KEY]);
434
435
        if ($className[0] === '@') {
436
            $index = substr($className, 1);
437
            return $this->objectMapping[$index];
438
        } elseif (!class_exists($className)) {
439
            throw new JsonSerializerException('Unable to find class ' . $className);
440
        } elseif (($serialier = $this->resolveEntitySerializer($className)) != null) {
441
            $obj = $serialier->unserialize($value);
442
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
443
            return $obj;
444
        }
445
446
        list($ref, $object) = $this->getObjectInstance($className);
447
        $this->objectMapping[$this->objectMappingIndex++] = $object;
448
        foreach ($value as $property => $propertyValue) {
449
            try {
450
                $propRef = $ref->getProperty($property);
451
                $propRef->setAccessible(true);
452
                $propRef->setValue($object, $this->unserializeData($propertyValue));
453
            } catch (ReflectionException $e) {
454
                $object->$property = $this->unserializeData($propertyValue);
455
            }
456
        }
457
458
        if (method_exists($object, '__wakeup')) {
459
            $object->__wakeup();
460
        }
461
        return $object;
462
    }
463
464
    /**
465
     * @param $className
466
     * @return object
467
     */
468
    protected function getObjectInstance($className)
469
    {
470
        $ref = new ReflectionClass($className);
471
        return [$ref, $ref->newInstanceWithoutConstructor()];
472
    }
473
474
    /**
475
     * @param $value
476
     * @return array
477
     */
478
    protected function serializeClosure($value)
479
    {
480
        if (($serializer = $this->resolveEntitySerializer(\Closure::class)) == null) {
481
            throw new JsonSerializerException('Closure serializer not provided to unserialize closure');
482
        }
483
        return $serializer->serialize($value);
484
    }
485
486
    /**
487
     * @param $value
488
     * @return mixed
489
     */
490
    protected function unserializeClosure($value)
491
    {
492
        if (($serializer = $this->resolveEntitySerializer(\Closure::class)) == null) {
493
            throw new JsonSerializerException('Closure serializer not provided to unserialize closure');
494
        }
495
        return $serializer->unserialize($value);
496
    }
497
498
    /**
499
     * Reset variables
500
     *
501
     * @return void
502
     */
503
    protected function reset()
504
    {
505
        $this->objectStorage = new SplObjectStorage();
506
        $this->objectMapping = [];
507
        $this->objectMappingIndex = 0;
508
    }
509
}
510