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 (#24)
by
unknown
02:05
created

JsonSerializer::reset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
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
     * Constructor.
62
     *
63
     * @param ClosureSerializerInterface $closureSerializer
64
     */
65
    public function __construct(ClosureSerializerInterface $closureSerializer = null)
66
    {
67
        $this->preserveZeroFractionSupport = defined('JSON_PRESERVE_ZERO_FRACTION');
68
        $this->closureSerializer = $closureSerializer;
69
    }
70
71
    /**
72
     * Serialize the value in JSON
73
     *
74
     * @param mixed $value
75
     * @return string JSON encoded
76
     * @throws JsonSerializerException
77
     */
78
    public function serialize($value)
79
    {
80
        $this->reset();
81
        $serializedData = $this->serializeData($value);
82
        $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
83
        if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
84
            if (json_last_error() != JSON_ERROR_UTF8) {
85
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
86
            }
87
88
            $serializedData = $this->encodeNonUtf8ToUtf8($serializedData);
89
            $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
90
91
            if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
92
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
93
            }
94
        }
95
        return $this->processEncodedValue($encoded);
96
    }
97
98
    /**
99
     * Calculate encoding options
100
     *
101
     * @return integer
102
     */
103
    protected function calculateEncodeOptions()
104
    {
105
        $options = JSON_UNESCAPED_UNICODE;
106
        if ($this->preserveZeroFractionSupport) {
107
            $options |= JSON_PRESERVE_ZERO_FRACTION;
108
        }
109
        return $options;
110
    }
111
112
    /**
113
     * @param mixed $serializedData
114
     *
115
     * @return array
116
     */
117
    protected function encodeNonUtf8ToUtf8($serializedData)
118
    {
119
        if (is_string($serializedData)) {
120
            if (!mb_check_encoding($serializedData, 'UTF-8')) {
121
                $serializedData = [
122
                    static::SCALAR_IDENTIFIER_KEY => mb_convert_encoding($serializedData, 'UTF-8', '8bit'),
123
                    static::UTF8ENCODED_IDENTIFIER_KEY => static::VALUE_UTF8ENCODED,
124
                ];
125
            }
126
127
            return $serializedData;
128
        }
129
130
        $encodedKeys = [];
131
        $encodedData = [];
132
        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...
133
            if (is_array($value)) {
134
                $value = $this->encodeNonUtf8ToUtf8($value);
135
            }
136
137
            if (!mb_check_encoding($key, 'UTF-8')) {
138
                $key = mb_convert_encoding($key, 'UTF-8', '8bit');
139
                $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::KEY_UTF8ENCODED;
140
            }
141
142
            if (is_string($value)) {
143
                if (!mb_check_encoding($value, 'UTF-8')) {
144
                    $value = mb_convert_encoding($value, 'UTF-8', '8bit');
145
                    $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::VALUE_UTF8ENCODED;
146
                }
147
            }
148
149
            $encodedData[$key] = $value;
150
        }
151
152
        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...
153
            $encodedData[self::UTF8ENCODED_IDENTIFIER_KEY] = $encodedKeys;
154
        }
155
156
        return $encodedData;
157
    }
158
159
    /**
160
     * Execute post-encoding actions
161
     *
162
     * @param string $encoded
163
     * @return string
164
     */
165
    protected function processEncodedValue($encoded)
166
    {
167
        if (!$this->preserveZeroFractionSupport) {
168
            $encoded = preg_replace('/"' . static::FLOAT_ADAPTER . '\((.*?)\)"/', '\1', $encoded);
169
        }
170
        return $encoded;
171
    }
172
173
    /**
174
     * Unserialize the value from JSON
175
     *
176
     * @param string $value
177
     * @return mixed
178
     */
179
    public function unserialize($value)
180
    {
181
        $this->reset();
182
        $data = json_decode($value, true);
183
        if ($data === null && json_last_error() != JSON_ERROR_NONE) {
184
            throw new JsonSerializerException('Invalid JSON to unserialize.');
185
        }
186
        return $this->unserializeData($data);
187
    }
188
189
    /**
190
     * Parse the data to be json encoded
191
     *
192
     * @param mixed $value
193
     * @return mixed
194
     * @throws JsonSerializerException
195
     */
196
    protected function serializeData($value)
197
    {
198
        if (is_scalar($value) || $value === null) {
199
            if (!$this->preserveZeroFractionSupport && is_float($value) && ctype_digit((string)$value)) {
200
                // Because the PHP bug #50224, the float numbers with no
201
                // precision numbers are converted to integers when encoded
202
                $value = static::FLOAT_ADAPTER . '(' . $value . '.0)';
203
            }
204
            return $value;
205
        }
206
        if (is_resource($value)) {
207
            throw new JsonSerializerException('Resource is not supported in JsonSerializer');
208
        }
209
        if (is_array($value)) {
210
            return array_map(array($this, __FUNCTION__), $value);
211
        }
212
        if ($value instanceof \Closure) {
213
            if (!$this->closureSerializer) {
214
                throw new JsonSerializerException('Closure serializer not given. Unable to serialize closure.');
215
            }
216
            return array(
217
                static::CLOSURE_IDENTIFIER_KEY => true,
218
                'value' => $this->closureSerializer->serialize($value)
219
            );
220
        }
221
        return $this->serializeObject($value);
222
    }
223
224
    /**
225
     * Extract the data from an object
226
     *
227
     * @param object $value
228
     * @return array
229
     */
230
    protected function serializeObject($value)
231
    {
232
        $ref = new ReflectionClass($value);
233
234
        if ($this->objectStorage->contains($value)) {
235
            return array(static::CLASS_IDENTIFIER_KEY => '@' . $this->objectStorage[$value]);
236
        }
237
        $this->objectStorage->attach($value, $this->objectMappingIndex++);
238
239
        $paramsToSerialize = $this->getObjectProperties($ref, $value);
240
        $data = array(static::CLASS_IDENTIFIER_KEY => $ref->getName());
241
        $data += array_map(array($this, 'serializeData'), $this->extractObjectData($value, $ref, $paramsToSerialize));
242
        return $data;
243
    }
244
245
    /**
246
     * Return the list of properties to be serialized
247
     *
248
     * @param ReflectionClass $ref
249
     * @param object $value
250
     * @return array
251
     */
252
    protected function getObjectProperties($ref, $value)
253
    {
254
        if (method_exists($value, '__sleep')) {
255
            return $value->__sleep();
256
        }
257
258
        $props = array();
259
        foreach ($ref->getProperties() as $prop) {
260
            $props[] = $prop->getName();
261
        }
262
        return array_unique(array_merge($props, array_keys(get_object_vars($value))));
263
    }
264
265
    /**
266
     * Extract the object data
267
     *
268
     * @param object $value
269
     * @param ReflectionClass $ref
270
     * @param array $properties
271
     * @return array
272
     */
273
    protected function extractObjectData($value, $ref, $properties)
274
    {
275
        $data = array();
276
        foreach ($properties as $property) {
277
            try {
278
                $propRef = $ref->getProperty($property);
279
                $propRef->setAccessible(true);
280
                $data[$property] = $propRef->getValue($value);
281
            } catch (ReflectionException $e) {
282
                $data[$property] = $value->$property;
283
            }
284
        }
285
        return $data;
286
    }
287
288
    /**
289
     * Parse the json decode to convert to objects again
290
     *
291
     * @param mixed $value
292
     * @return mixed
293
     */
294
    protected function unserializeData($value)
295
    {
296
        $value = $this->decodeNonUtf8FromUtf8($value);
297
298
        if (is_scalar($value) || $value === null) {
299
            return $value;
300
        }
301
302
        if (isset($value[static::CLASS_IDENTIFIER_KEY])) {
303
            return $this->unserializeObject($value);
304
        }
305
306
        if (!empty($value[static::CLOSURE_IDENTIFIER_KEY])) {
307
            if (!$this->closureSerializer) {
308
                throw new JsonSerializerException('Closure serializer not provided to unserialize closure');
309
            }
310
            return $this->closureSerializer->unserialize($value['value']);
311
        }
312
313
        return array_map(array($this, __FUNCTION__), $value);
314
    }
315
316
    /**
317
     * @param mixed $serializedData
318
     *
319
     * @return mixed
320
     */
321
    protected function decodeNonUtf8FromUtf8($serializedData)
322
    {
323
        if (is_array($serializedData) && isset($serializedData[static::SCALAR_IDENTIFIER_KEY])) {
324
            $serializedData = mb_convert_encoding($serializedData[static::SCALAR_IDENTIFIER_KEY], '8bit', 'UTF-8');
325
            return $serializedData;
326
        } elseif (is_scalar($serializedData) || $serializedData === null) {
327
            return $serializedData;
328
        }
329
330
        $encodedKeys = [];
331
        if (isset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY])) {
332
            $encodedKeys = $serializedData[static::UTF8ENCODED_IDENTIFIER_KEY];
333
            unset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY]);
334
        }
335
336
        $decodedData = [];
337
        foreach ($serializedData as $key => $value) {
338
            if (is_array($value)) {
339
                $value = $this->decodeNonUtf8FromUtf8($value);
340
            }
341
342
            if (isset($encodedKeys[$key])) {
343
                $originalKey = $key;
344
                if ($encodedKeys[$key] & static::KEY_UTF8ENCODED) {
345
                    $key = mb_convert_encoding($key, '8bit', 'UTF-8');
346
                }
347
                if ($encodedKeys[$originalKey] & static::VALUE_UTF8ENCODED) {
348
                    $value = mb_convert_encoding($value, '8bit', 'UTF-8');
349
                }
350
            }
351
352
            $decodedData[$key] = $value;
353
        }
354
355
        return $decodedData;
356
    }
357
358
    /**
359
     * Convert the serialized array into an object
360
     *
361
     * @param array $value
362
     * @return object
363
     * @throws JsonSerializerException
364
     */
365
    protected function unserializeObject($value)
366
    {
367
        $className = $value[static::CLASS_IDENTIFIER_KEY];
368
        unset($value[static::CLASS_IDENTIFIER_KEY]);
369
370
        if ($className[0] === '@') {
371
            $index = substr($className, 1);
372
            return $this->objectMapping[$index];
373
        }
374
375
        if (!class_exists($className)) {
376
            throw new JsonSerializerException('Unable to find class ' . $className);
377
        }
378
379
        if ($className === 'DateTime') {
380
            $obj = $this->restoreUsingUnserialize($className, $value);
381
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
382
            return $obj;
383
        }
384
385
        $ref = new ReflectionClass($className);
386
        $obj = $ref->newInstanceWithoutConstructor();
387
        $this->objectMapping[$this->objectMappingIndex++] = $obj;
388
        foreach ($value as $property => $propertyValue) {
389
            try {
390
                $propRef = $ref->getProperty($property);
391
                $propRef->setAccessible(true);
392
                $propRef->setValue($obj, $this->unserializeData($propertyValue));
393
            } catch (ReflectionException $e) {
394
                $obj->$property = $this->unserializeData($propertyValue);
395
            }
396
        }
397
        if (method_exists($obj, '__wakeup')) {
398
            $obj->__wakeup();
399
        }
400
        return $obj;
401
    }
402
403
    protected function restoreUsingUnserialize($className, $attributes)
404
    {
405
        $obj = (object)$attributes;
406
        $serialized = preg_replace('|^O:\d+:"\w+":|', 'O:' . strlen($className) . ':"' . $className . '":', serialize($obj));
407
        return unserialize($serialized);
408
    }
409
410
    /**
411
     * Reset variables
412
     *
413
     * @return void
414
     */
415
    protected function reset()
416
    {
417
        $this->objectStorage = new SplObjectStorage();
418
        $this->objectMapping = array();
419
        $this->objectMappingIndex = 0;
420
    }
421
}
422