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 |
|
$normalizedObject = [$this->classAnnotation => get_class($object)]; |
92
|
10 |
|
$normalizedObject += array_map([$this, 'normalizeValue'], $this->extractObjectProperties($object)); |
93
|
|
|
|
94
|
10 |
|
return $normalizedObject; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Parse the data to be json encoded |
99
|
|
|
* |
100
|
|
|
* @param mixed $value |
101
|
|
|
* |
102
|
|
|
* @return mixed |
103
|
|
|
* @throws JsonSerializerException |
104
|
|
|
*/ |
105
|
8 |
|
private function normalizeValue($value) |
106
|
|
|
{ |
107
|
8 |
|
if (is_resource($value)) { |
108
|
|
|
throw new JsonSerializerException("Can't serialize PHP resources."); |
109
|
|
|
} |
110
|
|
|
|
111
|
8 |
|
if ($value instanceof \Closure) { |
112
|
|
|
throw new JsonSerializerException("Can't serialize closures."); |
113
|
|
|
} |
114
|
|
|
|
115
|
8 |
|
if (is_object($value)) { |
116
|
4 |
|
return $this->normalizeObject($value); |
117
|
|
|
} |
118
|
|
|
|
119
|
7 |
|
if (is_array($value)) { |
120
|
1 |
|
return array_map([$this, 'normalizeValue'], $value); |
121
|
|
|
} |
122
|
|
|
|
123
|
6 |
|
return $value; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Returns an array containing the object's properties to values |
128
|
|
|
* |
129
|
|
|
* @param object $object |
130
|
|
|
* |
131
|
|
|
* @return array |
132
|
|
|
*/ |
133
|
10 |
|
private function extractObjectProperties($object) |
134
|
|
|
{ |
135
|
10 |
|
$propertyToValue = []; |
136
|
|
|
|
137
|
10 |
|
if (method_exists($object, '__sleep')) { |
138
|
1 |
|
$properties = $object->__sleep(); |
139
|
1 |
|
foreach ($properties as $property) { |
140
|
1 |
|
$propertyToValue[$property] = $object->$property; |
141
|
1 |
|
} |
142
|
|
|
|
143
|
1 |
|
return $propertyToValue; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
|
147
|
9 |
|
$reflectedProperties = []; |
148
|
9 |
|
$ref = new \ReflectionClass($object); |
149
|
9 |
|
foreach ($ref->getProperties() as $property) { |
150
|
2 |
|
$property->setAccessible(true); |
151
|
2 |
|
$propertyToValue[$property->getName()] = $property->getValue($object); |
152
|
2 |
|
$reflectedProperties[] = $property->getName(); |
153
|
9 |
|
} |
154
|
|
|
|
155
|
9 |
|
$dynamicProperties = array_diff(array_keys(get_object_vars($object)), $reflectedProperties); |
156
|
|
|
|
157
|
9 |
|
foreach ($dynamicProperties as $property) { |
158
|
5 |
|
$propertyToValue[$property] = $object->$property; |
159
|
9 |
|
} |
160
|
|
|
|
161
|
9 |
|
return $propertyToValue; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Parse the json decode to convert to objects again |
166
|
|
|
* |
167
|
|
|
* @param mixed $data |
168
|
|
|
* |
169
|
|
|
* @return mixed |
170
|
|
|
*/ |
171
|
10 |
|
private function denormalizeData($data) |
172
|
|
|
{ |
173
|
10 |
|
if (is_scalar($data) || $data === null) { |
174
|
4 |
|
return $data; |
175
|
|
|
} |
176
|
|
|
|
177
|
10 |
|
if (isset($data[$this->classAnnotation])) { |
178
|
10 |
|
return $this->denormalizeObject($data); |
|
|
|
|
179
|
|
|
} |
180
|
|
|
|
181
|
1 |
|
return array_map([$this, 'denormalizeData'], $data); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Convert the serialized array into an object |
186
|
|
|
* |
187
|
|
|
* @param array $data |
188
|
|
|
* |
189
|
|
|
* @return object |
190
|
|
|
* @throws JsonSerializerException |
191
|
|
|
*/ |
192
|
10 |
|
private function denormalizeObject(array $data) |
193
|
|
|
{ |
194
|
10 |
|
$className = $data[$this->classAnnotation]; |
195
|
10 |
|
unset($data[$this->classAnnotation]); |
196
|
|
|
|
197
|
10 |
|
if ($className[0] === '@') { |
198
|
2 |
|
$index = substr($className, 1); |
199
|
2 |
|
return $this->indexToObject[$index]; |
200
|
|
|
} |
201
|
|
|
|
202
|
10 |
|
if (!class_exists($className)) { |
203
|
1 |
|
throw new JsonSerializerException("Unable to find class $className for deserialization."); |
204
|
|
|
} |
205
|
|
|
|
206
|
9 |
|
if ($className === 'DateTime') { |
207
|
1 |
|
$object = $this->denormalizeDateTime($className, $data); |
208
|
1 |
|
$this->indexToObject[$this->objectIndex++] = $object; |
209
|
1 |
|
return $object; |
210
|
|
|
} |
211
|
|
|
|
212
|
8 |
|
$ref = new \ReflectionClass($className); |
213
|
8 |
|
$object = $ref->newInstanceWithoutConstructor(); |
214
|
8 |
|
$this->indexToObject[$this->objectIndex++] = $object; |
215
|
8 |
|
foreach ($data as $property => $propertyValue) { |
216
|
6 |
|
if ($ref->hasProperty($property)) { |
217
|
2 |
|
$propRef = $ref->getProperty($property); |
218
|
2 |
|
$propRef->setAccessible(true); |
219
|
2 |
|
$propRef->setValue($object, $this->denormalizeData($propertyValue)); |
220
|
2 |
|
} else { |
221
|
4 |
|
$object->$property = $this->denormalizeData($propertyValue); |
222
|
|
|
} |
223
|
8 |
|
} |
224
|
|
|
|
225
|
8 |
|
if (method_exists($object, '__wakeup')) { |
226
|
1 |
|
$object->__wakeup(); |
227
|
1 |
|
} |
228
|
|
|
|
229
|
8 |
|
return $object; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* @param string $className |
234
|
|
|
* @param array $attributes |
235
|
|
|
* |
236
|
|
|
* @return \DateTime |
237
|
|
|
*/ |
238
|
1 |
|
private function denormalizeDateTime($className, array $attributes) |
239
|
|
|
{ |
240
|
1 |
|
$obj = (object)$attributes; |
241
|
1 |
|
$serialized = preg_replace( |
242
|
1 |
|
'|^O:\d+:"\w+":|', |
243
|
1 |
|
'O:' . strlen($className) . ':"' . $className . '":', |
244
|
1 |
|
serialize($obj) |
245
|
1 |
|
); |
246
|
1 |
|
return unserialize($serialized); |
247
|
|
|
} |
248
|
|
|
} |
249
|
|
|
|
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.