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
02:23
created

resolveEntitySerializerByDirectClassName()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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