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
Push — master ( 68c6a7...457e68 )
by Juan
8s
created

JsonSerializer::encodeNonUtf8ToUtf8()   C

Complexity

Conditions 11
Paths 52

Size

Total Lines 41
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 41
rs 5.2653
cc 11
eloc 23
nc 52
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
187
        if (mb_strpos($value, static::UTF8ENCODED_IDENTIFIER_KEY) !== false) {
188
            $data = $this->decodeNonUtf8FromUtf8($data);
189
        }
190
191
        return $this->unserializeData($data);
192
    }
193
194
    /**
195
     * Parse the data to be json encoded
196
     *
197
     * @param mixed $value
198
     * @return mixed
199
     * @throws JsonSerializerException
200
     */
201
    protected function serializeData($value)
202
    {
203
        if (is_scalar($value) || $value === null) {
204
            if (!$this->preserveZeroFractionSupport && is_float($value) && ctype_digit((string)$value)) {
205
                // Because the PHP bug #50224, the float numbers with no
206
                // precision numbers are converted to integers when encoded
207
                $value = static::FLOAT_ADAPTER . '(' . $value . '.0)';
208
            }
209
            return $value;
210
        }
211
        if (is_resource($value)) {
212
            throw new JsonSerializerException('Resource is not supported in JsonSerializer');
213
        }
214
        if (is_array($value)) {
215
            return array_map(array($this, __FUNCTION__), $value);
216
        }
217
        if ($value instanceof \Closure) {
218
            if (!$this->closureSerializer) {
219
                throw new JsonSerializerException('Closure serializer not given. Unable to serialize closure.');
220
            }
221
            return array(
222
                static::CLOSURE_IDENTIFIER_KEY => true,
223
                'value' => $this->closureSerializer->serialize($value)
224
            );
225
        }
226
        return $this->serializeObject($value);
227
    }
228
229
    /**
230
     * Extract the data from an object
231
     *
232
     * @param object $value
233
     * @return array
234
     */
235
    protected function serializeObject($value)
236
    {
237
        $ref = new ReflectionClass($value);
238
239
        if ($this->objectStorage->contains($value)) {
240
            return array(static::CLASS_IDENTIFIER_KEY => '@' . $this->objectStorage[$value]);
241
        }
242
        $this->objectStorage->attach($value, $this->objectMappingIndex++);
243
244
        $paramsToSerialize = $this->getObjectProperties($ref, $value);
245
        $data = array(static::CLASS_IDENTIFIER_KEY => $ref->getName());
246
        $data += array_map(array($this, 'serializeData'), $this->extractObjectData($value, $ref, $paramsToSerialize));
247
        return $data;
248
    }
249
250
    /**
251
     * Return the list of properties to be serialized
252
     *
253
     * @param ReflectionClass $ref
254
     * @param object $value
255
     * @return array
256
     */
257
    protected function getObjectProperties($ref, $value)
258
    {
259
        if (method_exists($value, '__sleep')) {
260
            return $value->__sleep();
261
        }
262
263
        $props = array();
264
        foreach ($ref->getProperties() as $prop) {
265
            $props[] = $prop->getName();
266
        }
267
        return array_unique(array_merge($props, array_keys(get_object_vars($value))));
268
    }
269
270
    /**
271
     * Extract the object data
272
     *
273
     * @param object $value
274
     * @param ReflectionClass $ref
275
     * @param array $properties
276
     * @return array
277
     */
278
    protected function extractObjectData($value, $ref, $properties)
279
    {
280
        $data = array();
281
        foreach ($properties as $property) {
282
            try {
283
                $propRef = $ref->getProperty($property);
284
                $propRef->setAccessible(true);
285
                $data[$property] = $propRef->getValue($value);
286
            } catch (ReflectionException $e) {
287
                $data[$property] = $value->$property;
288
            }
289
        }
290
        return $data;
291
    }
292
293
    /**
294
     * Parse the json decode to convert to objects again
295
     *
296
     * @param mixed $value
297
     * @return mixed
298
     */
299
    protected function unserializeData($value)
300
    {
301
        if (is_scalar($value) || $value === null) {
302
            return $value;
303
        }
304
305
        if (isset($value[static::CLASS_IDENTIFIER_KEY])) {
306
            return $this->unserializeObject($value);
0 ignored issues
show
Bug introduced by
It seems like $value defined by parameter $value on line 299 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...
307
        }
308
309
        if (!empty($value[static::CLOSURE_IDENTIFIER_KEY])) {
310
            if (!$this->closureSerializer) {
311
                throw new JsonSerializerException('Closure serializer not provided to unserialize closure');
312
            }
313
            return $this->closureSerializer->unserialize($value['value']);
314
        }
315
316
        return array_map(array($this, __FUNCTION__), $value);
317
    }
318
319
    /**
320
     * @param mixed $serializedData
321
     *
322
     * @return mixed
323
     */
324
    protected function decodeNonUtf8FromUtf8($serializedData)
325
    {
326
        if (is_array($serializedData) && isset($serializedData[static::SCALAR_IDENTIFIER_KEY])) {
327
            $serializedData = mb_convert_encoding($serializedData[static::SCALAR_IDENTIFIER_KEY], '8bit', 'UTF-8');
328
            return $serializedData;
329
        } elseif (is_scalar($serializedData) || $serializedData === null) {
330
            return $serializedData;
331
        }
332
333
        $encodedKeys = [];
334
        if (isset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY])) {
335
            $encodedKeys = $serializedData[static::UTF8ENCODED_IDENTIFIER_KEY];
336
            unset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY]);
337
        }
338
339
        $decodedData = [];
340
        foreach ($serializedData as $key => $value) {
341
            if (is_array($value)) {
342
                $value = $this->decodeNonUtf8FromUtf8($value);
343
            }
344
345
            if (isset($encodedKeys[$key])) {
346
                $originalKey = $key;
347
                if ($encodedKeys[$key] & static::KEY_UTF8ENCODED) {
348
                    $key = mb_convert_encoding($key, '8bit', 'UTF-8');
349
                }
350
                if ($encodedKeys[$originalKey] & static::VALUE_UTF8ENCODED) {
351
                    $value = mb_convert_encoding($value, '8bit', 'UTF-8');
352
                }
353
            }
354
355
            $decodedData[$key] = $value;
356
        }
357
358
        return $decodedData;
359
    }
360
361
    /**
362
     * Convert the serialized array into an object
363
     *
364
     * @param array $value
365
     * @return object
366
     * @throws JsonSerializerException
367
     */
368
    protected function unserializeObject($value)
369
    {
370
        $className = $value[static::CLASS_IDENTIFIER_KEY];
371
        unset($value[static::CLASS_IDENTIFIER_KEY]);
372
373
        if ($className[0] === '@') {
374
            $index = substr($className, 1);
375
            return $this->objectMapping[$index];
376
        }
377
378
        if (!class_exists($className)) {
379
            throw new JsonSerializerException('Unable to find class ' . $className);
380
        }
381
382
        if ($className === 'DateTime') {
383
            $obj = $this->restoreUsingUnserialize($className, $value);
384
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
385
            return $obj;
386
        }
387
388
        $ref = new ReflectionClass($className);
389
        $obj = $ref->newInstanceWithoutConstructor();
390
        $this->objectMapping[$this->objectMappingIndex++] = $obj;
391
        foreach ($value as $property => $propertyValue) {
392
            try {
393
                $propRef = $ref->getProperty($property);
394
                $propRef->setAccessible(true);
395
                $propRef->setValue($obj, $this->unserializeData($propertyValue));
396
            } catch (ReflectionException $e) {
397
                $obj->$property = $this->unserializeData($propertyValue);
398
            }
399
        }
400
        if (method_exists($obj, '__wakeup')) {
401
            $obj->__wakeup();
402
        }
403
        return $obj;
404
    }
405
406
    protected function restoreUsingUnserialize($className, $attributes)
407
    {
408
        $obj = (object)$attributes;
409
        $serialized = preg_replace('|^O:\d+:"\w+":|', 'O:' . strlen($className) . ':"' . $className . '":', serialize($obj));
410
        return unserialize($serialized);
411
    }
412
413
    /**
414
     * Reset variables
415
     *
416
     * @return void
417
     */
418
    protected function reset()
419
    {
420
        $this->objectStorage = new SplObjectStorage();
421
        $this->objectMapping = array();
422
        $this->objectMappingIndex = 0;
423
    }
424
}
425