ObjectNormalizer::normalize()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.9666
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
namespace PSB\Core\Serialization\Json;
3
4
5
use PSB\Core\Exception\JsonSerializerException;
6
use PSB\Core\Util\Guard;
7
8
class ObjectNormalizer
9
{
10
    /**
11
     * @var string
12
     */
13
    private $classAnnotation;
14
15
    /**
16
     * Maps objects to their corresponding index. Used to deconstruct cyclic references when normalizing.
17
     *
18
     * @var \SplObjectStorage
19
     */
20
    private $objectToIndex;
21
22
    /**
23
     * Maps indexes to their corresponding object. Used to reconstruct cyclic references when de-normalizing.
24
     *
25
     * @var array
26
     */
27
    private $indexToObject = [];
28
29
    /**
30
     * @var integer
31
     */
32
    private $objectIndex = 0;
33
34
35
    /**
36
     * @param string $classAnnotation
37
     */
38 21
    public function __construct($classAnnotation = '@type')
39
    {
40 21
        Guard::againstNullAndEmpty('classAnnotation', $classAnnotation);
41 21
        $this->classAnnotation = $classAnnotation;
42 21
    }
43
44
    /**
45
     * @param object $object
46
     *
47
     * @return array
48
     */
49 11
    public function normalize($object)
50
    {
51 11
        if (!is_object($object)) {
52 1
            throw new JsonSerializerException("Can only serialize objects.");
53
        }
54
55 10
        $this->reset();
56 10
        return $this->normalizeObject($object);
57
    }
58
59
    /**
60
     * @param array $data
61
     *
62
     * @return object
63
     */
64 10
    public function denormalize(array $data)
65
    {
66 10
        $this->reset();
67 10
        return $this->denormalizeData($data);
68
    }
69
70 19
    private function reset()
71
    {
72 19
        $this->objectToIndex = new \SplObjectStorage();
73 19
        $this->indexToObject = [];
74 19
        $this->objectIndex = 0;
75 19
    }
76
77
    /**
78
     * Extract the data from an object
79
     *
80
     * @param object $object
81
     *
82
     * @return array
83
     */
84 10
    private function normalizeObject($object)
85
    {
86 10
        if ($this->objectToIndex->contains($object)) {
87 2
            return [$this->classAnnotation => '@' . $this->objectToIndex[$object]];
88
        }
89 10
        $this->objectToIndex->attach($object, $this->objectIndex++);
90
91 10
        $className = get_class($object);
92 10
        $normalizedObject = [$this->classAnnotation => $className];
93 10
        if ($className === 'DateTime') {
94 1
            $normalizedObject += (array) $object;
95
        } else {
96 9
            $normalizedObject += array_map([$this, 'normalizeValue'], $this->extractObjectProperties($object));
97
        }
98
99 10
        return $normalizedObject;
100
    }
101
102
    /**
103
     * Parse the data to be json encoded
104
     *
105
     * @param mixed $value
106
     *
107
     * @return mixed
108
     * @throws JsonSerializerException
109
     */
110 7
    private function normalizeValue($value)
111
    {
112 7
        if (is_resource($value)) {
113
            throw new JsonSerializerException("Can't serialize PHP resources.");
114
        }
115
116 7
        if ($value instanceof \Closure) {
117
            throw new JsonSerializerException("Can't serialize closures.");
118
        }
119
120 7
        if (is_object($value)) {
121 4
            return $this->normalizeObject($value);
122
        }
123
124 6
        if (is_array($value)) {
125 1
            return array_map([$this, 'normalizeValue'], $value);
126
        }
127
128 5
        return $value;
129
    }
130
131
    /**
132
     * Returns an array containing the object's properties to values
133
     *
134
     * @param object $object
135
     *
136
     * @return array
137
     */
138 9
    private function extractObjectProperties($object)
139
    {
140 9
        $propertyToValue = [];
141
142 9
        if (method_exists($object, '__sleep')) {
143 1
            $properties = $object->__sleep();
144 1
            foreach ($properties as $property) {
145 1
                $propertyToValue[$property] = $object->$property;
146
            }
147
148 1
            return $propertyToValue;
149
        }
150
151
152 8
        $reflectedProperties = [];
153 8
        $ref = new \ReflectionClass($object);
154 8
        foreach ($ref->getProperties() as $property) {
155 2
            $property->setAccessible(true);
156 2
            $propertyToValue[$property->getName()] = $property->getValue($object);
157 2
            $reflectedProperties[] = $property->getName();
158
        }
159
160 8
        $dynamicProperties = array_diff(array_keys(get_object_vars($object)), $reflectedProperties);
161
162 8
        foreach ($dynamicProperties as $property) {
163 4
            $propertyToValue[$property] = $object->$property;
164
        }
165
166 8
        return $propertyToValue;
167
    }
168
169
    /**
170
     * Parse the json decode to convert to objects again
171
     *
172
     * @param mixed $data
173
     *
174
     * @return mixed
175
     */
176 10
    private function denormalizeData($data)
177
    {
178 10
        if (is_scalar($data) || $data === null) {
179 4
            return $data;
180
        }
181
182 10
        if (isset($data[$this->classAnnotation])) {
183 10
            return $this->denormalizeObject($data);
184
        }
185
186 1
        return array_map([$this, 'denormalizeData'], $data);
187
    }
188
189
    /**
190
     * Convert the serialized array into an object
191
     *
192
     * @param array $data
193
     *
194
     * @return object
195
     * @throws JsonSerializerException
196
     */
197 10
    private function denormalizeObject(array $data)
198
    {
199 10
        $className = $data[$this->classAnnotation];
200 10
        unset($data[$this->classAnnotation]);
201
202 10
        if ($className[0] === '@') {
203 2
            $index = substr($className, 1);
204 2
            return $this->indexToObject[$index];
205
        }
206
207 10
        if (!class_exists($className)) {
208 1
            throw new JsonSerializerException("Unable to find class $className for deserialization.");
209
        }
210
211 9
        if ($className === 'DateTime') {
212 1
            $object = $this->denormalizeDateTime($className, $data);
213 1
            $this->indexToObject[$this->objectIndex++] = $object;
214 1
            return $object;
215
        }
216
217 8
        $ref = new \ReflectionClass($className);
218 8
        $object = $ref->newInstanceWithoutConstructor();
219 8
        $this->indexToObject[$this->objectIndex++] = $object;
220 8
        foreach ($data as $property => $propertyValue) {
221 6
            if ($ref->hasProperty($property)) {
222 2
                $propRef = $ref->getProperty($property);
223 2
                $propRef->setAccessible(true);
224 2
                $propRef->setValue($object, $this->denormalizeData($propertyValue));
225
            } else {
226 4
                $object->$property = $this->denormalizeData($propertyValue);
227
            }
228
        }
229
230 8
        if (method_exists($object, '__wakeup')) {
231 1
            $object->__wakeup();
232
        }
233
234 8
        return $object;
235
    }
236
237
    /**
238
     * @param string $className
239
     * @param array  $attributes
240
     *
241
     * @return \DateTime
242
     */
243 1
    private function denormalizeDateTime($className, array $attributes)
244
    {
245 1
        $obj = (object)$attributes;
246 1
        $serialized = preg_replace(
247 1
            '|^O:\d+:"\w+":|',
248 1
            'O:' . strlen($className) . ':"' . $className . '":',
249 1
            serialize($obj)
250
        );
251
252 1
        return unserialize($serialized);
253
    }
254
}
255