Serializer::unserializeUserDefinedObject()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 14
rs 9.4286
cc 2
eloc 8
nc 2
nop 2
1
<?php
2
3
namespace NilPortugues\Serializer;
4
5
use Closure;
6
use NilPortugues\Serializer\Strategy\StrategyInterface;
7
use ReflectionClass;
8
use ReflectionException;
9
use SplObjectStorage;
10
11
class Serializer
12
{
13
    const CLASS_IDENTIFIER_KEY = '@type';
14
    const SCALAR_TYPE = '@scalar';
15
    const SCALAR_VALUE = '@value';
16
    const NULL_VAR = null;
17
    const MAP_TYPE = '@map';
18
19
    /**
20
     * Storage for object.
21
     *
22
     * Used for recursion
23
     *
24
     * @var SplObjectStorage
25
     */
26
    protected static $objectStorage;
27
28
    /**
29
     * Object mapping for recursion.
30
     *
31
     * @var array
32
     */
33
    protected static $objectMapping = [];
34
35
    /**
36
     * Object mapping index.
37
     *
38
     * @var int
39
     */
40
    protected static $objectMappingIndex = 0;
41
42
    /**
43
     * @var \NilPortugues\Serializer\Strategy\StrategyInterface|\NilPortugues\Serializer\Strategy\JsonStrategy
44
     */
45
    protected $serializationStrategy;
46
47
    /**
48
     * @var array
49
     */
50
    private $dateTimeClassType = ['DateTime', 'DateTimeImmutable', 'DateTimeZone', 'DateInterval', 'DatePeriod'];
51
52
    /**
53
     * @var array
54
     */
55
    protected $serializationMap = [
56
        'array' => 'serializeArray',
57
        'integer' => 'serializeScalar',
58
        'double' => 'serializeScalar',
59
        'boolean' => 'serializeScalar',
60
        'string' => 'serializeScalar',
61
    ];
62
63
    /**
64
     * @var bool
65
     */
66
    protected $isHHVM;
67
68
    /**
69
     * Hack specific serialization classes.
70
     *
71
     * @var array
72
     */
73
    protected $unserializationMapHHVM = [];
74
75
    /**
76
     * @param StrategyInterface $strategy
77
     */
78
    public function __construct(StrategyInterface $strategy)
79
    {
80
        $this->isHHVM = \defined('HHVM_VERSION');
81
        if ($this->isHHVM) {
82
            // @codeCoverageIgnoreStart
83
            $this->serializationMap = \array_merge(
84
                $this->serializationMap,
85
                include \realpath(\dirname(__FILE__).'/Mapping/serialization_hhvm.php')
86
            );
87
            $this->unserializationMapHHVM = include \realpath(\dirname(__FILE__).'/Mapping/unserialization_hhvm.php');
88
            // @codeCoverageIgnoreEnd
89
        }
90
        $this->serializationStrategy = $strategy;
91
    }
92
93
    /**
94
     * This is handly specially in order to add additional data before the
95
     * serialization process takes place using the transformer public methods, if any.
96
     *
97
     * @return StrategyInterface
98
     */
99
    public function getTransformer()
100
    {
101
        return $this->serializationStrategy;
102
    }
103
104
    /**
105
     * Serialize the value in JSON.
106
     *
107
     * @param mixed $value
108
     *
109
     * @return string JSON encoded
110
     *
111
     * @throws SerializerException
112
     */
113
    public function serialize($value)
114
    {
115
        $this->reset();
116
117
        return $this->serializationStrategy->serialize($this->serializeData($value));
118
    }
119
120
    /**
121
     * Reset variables.
122
     */
123
    protected function reset()
124
    {
125
        self::$objectStorage = new SplObjectStorage();
126
        self::$objectMapping = [];
127
        self::$objectMappingIndex = 0;
128
    }
129
130
    /**
131
     * Parse the data to be json encoded.
132
     *
133
     * @param mixed $value
134
     *
135
     * @return mixed
136
     *
137
     * @throws SerializerException
138
     */
139
    protected function serializeData($value)
140
    {
141
        $this->guardForUnsupportedValues($value);
142
143
        if ($this->isHHVM && ($value instanceof \DateTimeZone || $value instanceof \DateInterval)) {
144
            // @codeCoverageIgnoreStart
145
            return \call_user_func_array($this->serializationMap[get_class($value)], [$this, $value]);
146
            // @codeCoverageIgnoreEnd
147
        }
148
149
        if (\is_object($value)) {
150
            return $this->serializeObject($value);
151
        }
152
153
        $type = (\gettype($value) && $value !== null) ? \gettype($value) : 'string';
154
        $func = $this->serializationMap[$type];
155
156
        return $this->$func($value);
157
    }
158
159
    /**
160
     * @param mixed $value
161
     *
162
     * @throws SerializerException
163
     */
164
    protected function guardForUnsupportedValues($value)
165
    {
166
        if ($value instanceof Closure) {
167
            throw new SerializerException('Closures are not supported in Serializer');
168
        }
169
170
        if ($value instanceof \DatePeriod) {
171
            throw new SerializerException(
172
                'DatePeriod is not supported in Serializer. Loop through it and serialize the output.'
173
            );
174
        }
175
176
        if (\is_resource($value)) {
177
            throw new SerializerException('Resource is not supported in Serializer');
178
        }
179
    }
180
181
    /**
182
     * Unserialize the value from string.
183
     *
184
     * @param mixed $value
185
     *
186
     * @return mixed
187
     */
188
    public function unserialize($value)
189
    {
190
        if (\is_array($value) && isset($value[self::SCALAR_TYPE])) {
191
            return $this->unserializeData($value);
192
        }
193
194
        $this->reset();
195
196
        return $this->unserializeData($this->serializationStrategy->unserialize($value));
197
    }
198
199
    /**
200
     * Parse the json decode to convert to objects again.
201
     *
202
     * @param mixed $value
203
     *
204
     * @return mixed
205
     */
206
    protected function unserializeData($value)
207
    {
208
        if ($value === null || !is_array($value)) {
209
            return $value;
210
        }
211
212
        if (isset($value[self::MAP_TYPE]) && !isset($value[self::CLASS_IDENTIFIER_KEY])) {
213
            $value = $value[self::SCALAR_VALUE];
214
215
            return $this->unserializeData($value);
216
        }
217
218
        if (isset($value[self::SCALAR_TYPE])) {
219
            return $this->getScalarValue($value);
220
        }
221
222
        if (isset($value[self::CLASS_IDENTIFIER_KEY])) {
223
            return $this->unserializeObject($value);
224
        }
225
226
        return \array_map([$this, __FUNCTION__], $value);
227
    }
228
229
    /**
230
     * @param $value
231
     *
232
     * @return float|int|null|bool
233
     */
234
    protected function getScalarValue($value)
235
    {
236
        switch ($value[self::SCALAR_TYPE]) {
237
            case 'integer':
238
                return \intval($value[self::SCALAR_VALUE]);
239
            case 'float':
240
                return \floatval($value[self::SCALAR_VALUE]);
241
            case 'boolean':
242
                return $value[self::SCALAR_VALUE];
243
            case 'NULL':
244
                return self::NULL_VAR;
245
        }
246
247
        return $value[self::SCALAR_VALUE];
248
    }
249
250
    /**
251
     * Convert the serialized array into an object.
252
     *
253
     * @param array $value
254
     *
255
     * @return object
256
     *
257
     * @throws SerializerException
258
     */
259
    protected function unserializeObject(array $value)
260
    {
261
        $className = $value[self::CLASS_IDENTIFIER_KEY];
262
        unset($value[self::CLASS_IDENTIFIER_KEY]);
263
264
        if (isset($value[self::MAP_TYPE])) {
265
            unset($value[self::MAP_TYPE]);
266
            unset($value[self::SCALAR_VALUE]);
267
        }
268
269
        if ($className[0] === '@') {
270
            return self::$objectMapping[substr($className, 1)];
271
        }
272
273
        if (!class_exists($className)) {
274
            throw new SerializerException('Unable to find class '.$className);
275
        }
276
277
        return (null === ($obj = $this->unserializeDateTimeFamilyObject($value, $className)))
278
            ? $this->unserializeUserDefinedObject($value, $className) : $obj;
279
    }
280
281
    /**
282
     * @param array  $value
283
     * @param string $className
284
     *
285
     * @return mixed
286
     */
287
    protected function unserializeDateTimeFamilyObject(array $value, $className)
288
    {
289
        $obj = null;
290
291
        if ($this->isDateTimeFamilyObject($className)) {
292
            if ($this->isHHVM) {
293
                // @codeCoverageIgnoreStart
294
                return \call_user_func_array(
295
                    $this->unserializationMapHHVM[$className],
296
                    [$this, $className, $value]
297
                );
298
                // @codeCoverageIgnoreEnd
299
            }
300
301
            $obj = $this->restoreUsingUnserialize($className, $value);
302
            self::$objectMapping[self::$objectMappingIndex++] = $obj;
303
        }
304
305
        return $obj;
306
    }
307
308
    /**
309
     * @param string $className
310
     *
311
     * @return bool
312
     */
313
    protected function isDateTimeFamilyObject($className)
314
    {
315
        $isDateTime = false;
316
317
        foreach ($this->dateTimeClassType as $class) {
318
            $isDateTime = $isDateTime || \is_subclass_of($className, $class, true) || $class === $className;
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
319
        }
320
321
        return $isDateTime;
322
    }
323
324
    /**
325
     * @param string $className
326
     * @param array  $attributes
327
     *
328
     * @return mixed
329
     */
330
    protected function restoreUsingUnserialize($className, array $attributes)
331
    {
332
        foreach ($attributes as &$attribute) {
333
            $attribute = $this->unserializeData($attribute);
334
        }
335
336
        $obj = (object) $attributes;
337
        $serialized = \preg_replace(
338
            '|^O:\d+:"\w+":|',
339
            'O:'.strlen($className).':"'.$className.'":',
340
            \serialize($obj)
341
        );
342
343
        return \unserialize($serialized);
344
    }
345
346
    /**
347
     * @param array  $value
348
     * @param string $className
349
     *
350
     * @return object
351
     */
352
    protected function unserializeUserDefinedObject(array $value, $className)
353
    {
354
        $ref = new ReflectionClass($className);
355
        $obj = $ref->newInstanceWithoutConstructor();
356
357
        self::$objectMapping[self::$objectMappingIndex++] = $obj;
358
        $this->setUnserializedObjectProperties($value, $ref, $obj);
359
360
        if (\method_exists($obj, '__wakeup')) {
361
            $obj->__wakeup();
362
        }
363
364
        return $obj;
365
    }
366
367
    /**
368
     * @param array           $value
369
     * @param ReflectionClass $ref
370
     * @param mixed           $obj
371
     *
372
     * @return mixed
373
     */
374
    protected function setUnserializedObjectProperties(array $value, ReflectionClass $ref, $obj)
375
    {
376
        foreach ($value as $property => $propertyValue) {
377
            try {
378
                $propRef = $ref->getProperty($property);
379
                $propRef->setAccessible(true);
380
                $propRef->setValue($obj, $this->unserializeData($propertyValue));
381
            } catch (ReflectionException $e) {
382
                $obj->$property = $this->unserializeData($propertyValue);
383
            }
384
        }
385
386
        return $obj;
387
    }
388
389
    /**
390
     * @param $value
391
     *
392
     * @return string
393
     */
394
    protected function serializeScalar($value)
395
    {
396
        $type = \gettype($value);
397
        if ($type === 'double') {
398
            $type = 'float';
399
        }
400
401
        return [
402
            self::SCALAR_TYPE => $type,
403
            self::SCALAR_VALUE => $value,
404
        ];
405
    }
406
407
    /**
408
     * @param array $value
409
     *
410
     * @return array
411
     */
412
    protected function serializeArray(array $value)
413
    {
414
        if (\array_key_exists(self::MAP_TYPE, $value)) {
415
            return $value;
416
        }
417
418
        $toArray = [self::MAP_TYPE => 'array', self::SCALAR_VALUE => []];
419
        foreach ($value as $key => $field) {
420
            $toArray[self::SCALAR_VALUE][$key] = $this->serializeData($field);
421
        }
422
423
        return $this->serializeData($toArray);
424
    }
425
426
    /**
427
     * Extract the data from an object.
428
     *
429
     * @param mixed $value
430
     *
431
     * @return array
432
     */
433
    protected function serializeObject($value)
434
    {
435
        if (self::$objectStorage->contains($value)) {
436
            return [self::CLASS_IDENTIFIER_KEY => '@'.self::$objectStorage[$value]];
437
        }
438
439
        self::$objectStorage->attach($value, self::$objectMappingIndex++);
440
441
        $reflection = new ReflectionClass($value);
442
        $className = $reflection->getName();
0 ignored issues
show
Bug introduced by
Consider using $reflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
443
444
        return $this->serializeInternalClass($value, $className, $reflection);
445
    }
446
447
    /**
448
     * @param mixed           $value
449
     * @param string          $className
450
     * @param ReflectionClass $ref
451
     *
452
     * @return array
453
     */
454
    protected function serializeInternalClass($value, $className, ReflectionClass $ref)
455
    {
456
        $paramsToSerialize = $this->getObjectProperties($ref, $value);
457
        $data = [self::CLASS_IDENTIFIER_KEY => $className];
458
        $data += \array_map([$this, 'serializeData'], $this->extractObjectData($value, $ref, $paramsToSerialize));
459
460
        return $data;
461
    }
462
463
    /**
464
     * Return the list of properties to be serialized.
465
     *
466
     * @param ReflectionClass $ref
467
     * @param $value
468
     *
469
     * @return array
470
     */
471
    protected function getObjectProperties(ReflectionClass $ref, $value)
472
    {
473
        $props = [];
474
        foreach ($ref->getProperties() as $prop) {
475
            $props[] = $prop->getName();
476
        }
477
478
        return \array_unique(\array_merge($props, \array_keys(\get_object_vars($value))));
479
    }
480
481
    /**
482
     * Extract the object data.
483
     *
484
     * @param mixed            $value
485
     * @param \ReflectionClass $rc
486
     * @param array            $properties
487
     *
488
     * @return array
489
     */
490
    protected function extractObjectData($value, ReflectionClass $rc, array $properties)
491
    {
492
        $data = [];
493
494
        $this->extractCurrentObjectProperties($value, $rc, $properties, $data);
495
        $this->extractAllInhertitedProperties($value, $rc, $data);
496
497
        return $data;
498
    }
499
500
    /**
501
     * @param mixed           $value
502
     * @param ReflectionClass $rc
503
     * @param array           $properties
504
     * @param array           $data
505
     */
506
    protected function extractCurrentObjectProperties($value, ReflectionClass $rc, array $properties, array &$data)
507
    {
508
        foreach ($properties as $propertyName) {
509
            try {
510
                $propRef = $rc->getProperty($propertyName);
511
                $propRef->setAccessible(true);
512
                $data[$propertyName] = $propRef->getValue($value);
513
            } catch (ReflectionException $e) {
514
                $data[$propertyName] = $value->$propertyName;
515
            }
516
        }
517
    }
518
519
    /**
520
     * @param mixed           $value
521
     * @param ReflectionClass $rc
522
     * @param array           $data
523
     */
524
    protected function extractAllInhertitedProperties($value, ReflectionClass $rc, array &$data)
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
525
    {
526
        do {
527
            $rp = array();
528
            /* @var $property \ReflectionProperty */
529
            foreach ($rc->getProperties() as $property) {
530
                $property->setAccessible(true);
531
                $rp[$property->getName()] = $property->getValue($this);
532
            }
533
            $data = \array_merge($rp, $data);
534
        } while ($rc = $rc->getParentClass());
535
    }
536
}
537