1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Majora\Framework\Normalizer; |
4
|
|
|
|
5
|
|
|
use Majora\Framework\Inflector\Inflector; |
6
|
|
|
use Majora\Framework\Model\EntityCollection; |
7
|
|
|
use Majora\Framework\Normalizer\Exception\InvalidScopeException; |
8
|
|
|
use Majora\Framework\Normalizer\Exception\ScopeNotFoundException; |
9
|
|
|
use Majora\Framework\Normalizer\Model\NormalizableInterface; |
10
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccess; |
11
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccessor; |
12
|
|
|
use Symfony\Component\PropertyAccess\PropertyPathInterface; |
13
|
|
|
use Symfony\Component\PropertyAccess\PropertyPath; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* Normalizer class implementing scoping compilation and object normalization construction. |
17
|
|
|
* |
18
|
|
|
* @see NormalizableInterface |
19
|
|
|
*/ |
20
|
|
|
class MajoraNormalizer |
21
|
|
|
{ |
22
|
|
|
/** |
23
|
|
|
* @var MajoraNormalizer[] |
24
|
|
|
*/ |
25
|
|
|
private static $instancePool; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var \ReflectionClass[] |
29
|
|
|
*/ |
30
|
|
|
private static $reflectionPool; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var PropertyPath[] |
34
|
|
|
*/ |
35
|
|
|
private $propertiesPathPool; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @var \Closure |
39
|
|
|
*/ |
40
|
|
|
private $extractorDelegate; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @var \Closure |
44
|
|
|
*/ |
45
|
|
|
private $readDelegate; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var \Closure |
49
|
|
|
*/ |
50
|
|
|
private $writeDelegate; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @var PropertyAccessor |
54
|
|
|
*/ |
55
|
|
|
protected $propertyAccessor; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* @var Inflector |
59
|
|
|
*/ |
60
|
|
|
protected $inflector; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Create and return an instantiated normalizer, returns always the same throught this call. |
64
|
|
|
* |
65
|
|
|
* @param string $key optionnal normalizer key |
66
|
|
|
* |
67
|
|
|
* @return MajoraNormalizer |
68
|
|
|
*/ |
69
|
36 |
|
public static function createNormalizer($key = 'default') |
70
|
|
|
{ |
71
|
36 |
|
return isset(self::$instancePool[$key]) ? |
72
|
36 |
|
self::$instancePool[$key] : |
73
|
2 |
|
self::$instancePool[$key] = new static( |
74
|
19 |
|
PropertyAccess::createPropertyAccessor() |
75
|
18 |
|
); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Construct. |
80
|
|
|
* |
81
|
|
|
* @param PropertyAccessor $propertyAccessor |
82
|
|
|
*/ |
83
|
2 |
|
public function __construct(PropertyAccessor $propertyAccessor) |
84
|
|
|
{ |
85
|
2 |
|
$this->propertyAccessor = $propertyAccessor; |
86
|
2 |
|
$this->inflector = new Inflector(); |
87
|
2 |
|
$this->propertiesPathPool = array(); |
88
|
2 |
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Create and return a Closure which can read all properties from an object. |
92
|
|
|
* |
93
|
|
|
* @return \Closure |
94
|
|
|
*/ |
95
|
|
|
private function createExtractorDelegate() |
96
|
|
|
{ |
97
|
|
|
return $this->extractorDelegate ?: $this->extractorDelegate = function () { |
98
|
|
|
return get_object_vars($this); |
99
|
|
|
}; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Create and return a Closure available to read an object property through a property path or a private property. |
104
|
|
|
* |
105
|
|
|
* @return \Closure |
106
|
|
|
*/ |
107
|
14 |
View Code Duplication |
private function createReadingDelegate() |
|
|
|
|
108
|
|
|
{ |
109
|
|
|
return $this->readDelegate ?: $this->readDelegate = function ($property, PropertyAccessor $propertyAccessor) { |
110
|
7 |
|
switch (true) { |
111
|
|
|
|
112
|
|
|
// Public property / accessor case |
113
|
14 |
|
case $propertyAccessor->isReadable($this, $property) : |
|
|
|
|
114
|
14 |
|
return $propertyAccessor->getValue($this, $property); |
115
|
|
|
|
116
|
|
|
// Private property / StdClass |
117
|
8 |
|
case property_exists($this, $property) || $this instanceof \StdClass: |
118
|
8 |
|
return $this->$property; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
throw new InvalidScopeException(sprintf( |
122
|
|
|
'Unable to read "%s" property from a "%s" object, any existing property path to read it in.', |
123
|
|
|
$property, |
124
|
|
|
get_class($this) |
125
|
|
|
)); |
126
|
14 |
|
}; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Normalize given object using given scope. |
131
|
|
|
* |
132
|
|
|
* @param mixed $object |
133
|
|
|
* @param string $scope |
134
|
|
|
* |
135
|
|
|
* @return array|string |
136
|
|
|
* |
137
|
|
|
* @throws ScopeNotFoundException If given scope not defined into given normalizable |
138
|
|
|
* @throws InvalidScopeException If given scope requires an unaccessible field |
139
|
|
|
*/ |
140
|
12 |
|
public function normalize($object, $scope = 'default') |
141
|
|
|
{ |
142
|
6 |
|
switch (true) { |
143
|
|
|
|
144
|
|
|
// Cannot normalized anything which already are |
145
|
12 |
|
case !is_object($object) : |
|
|
|
|
146
|
10 |
|
return $object; |
|
|
|
|
147
|
|
|
|
148
|
|
|
// StdClass can be cast as array |
149
|
10 |
|
case $object instanceof StdClass : |
|
|
|
|
150
|
|
|
return (array) $object; |
151
|
|
|
|
152
|
|
|
// DateTime : ISO format |
153
|
5 |
|
case $object instanceof \DateTime: |
154
|
4 |
|
return $object->format(\DateTime::ISO8601); |
155
|
|
|
|
156
|
|
|
// Other objects : we use a closure hack to read data |
157
|
10 |
|
case !$object instanceof NormalizableInterface : |
|
|
|
|
158
|
|
|
$extractor = \Closure::bind($this->createExtractorDelegate(), $object, get_class($object)); |
159
|
|
|
|
160
|
|
|
return $extractor($object); |
161
|
|
|
|
162
|
|
|
// At this point, we always got a Normalizable |
163
|
5 |
|
default: |
164
|
10 |
|
return $object->normalize($scope); |
165
|
5 |
|
} |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Normalize given normalizable following given scope. |
170
|
|
|
* |
171
|
|
|
* @param NormalizableInterface $object |
172
|
|
|
* @param string $scope |
173
|
|
|
* |
174
|
|
|
* @return array |
175
|
|
|
*/ |
176
|
16 |
|
public function scopify(NormalizableInterface $object, $scope) |
177
|
|
|
{ |
178
|
16 |
|
$scopes = $object->getScopes(); |
179
|
16 |
|
if (!isset($scopes[$scope])) { |
180
|
2 |
|
throw new ScopeNotFoundException(sprintf( |
181
|
2 |
|
'Invalid scope for %s object, only ["%s"] supported, "%s" given.', |
182
|
1 |
|
get_class($object), |
183
|
2 |
|
implode('", "', array_keys($scopes)), |
184
|
|
|
$scope |
185
|
1 |
|
)); |
186
|
|
|
} |
187
|
14 |
|
if (empty($scopes) || empty($scopes[$scope])) { |
188
|
|
|
return array(); |
189
|
|
|
} |
190
|
|
|
|
191
|
14 |
|
$read = \Closure::bind( |
192
|
14 |
|
$this->createReadingDelegate(), |
193
|
7 |
|
$object, |
194
|
7 |
|
get_class($object) |
195
|
7 |
|
); |
196
|
|
|
|
197
|
|
|
// simple value scope |
198
|
14 |
|
if (is_string($scopes[$scope])) { |
199
|
6 |
|
return $read($scopes[$scope], $this->propertyAccessor); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
// flatten fields |
203
|
10 |
|
$fields = array(); |
204
|
10 |
|
$stack = array($scopes[$scope]); |
205
|
|
|
do { |
206
|
10 |
|
$stackedField = array_shift($stack); |
207
|
10 |
|
foreach ($stackedField as $fieldConfig) { |
208
|
10 |
|
if (strpos($fieldConfig, '@') === 0) { |
209
|
6 |
|
if (!array_key_exists( |
210
|
6 |
|
$inheritedScope = str_replace('@', '', $fieldConfig), |
211
|
|
|
$scopes |
212
|
3 |
|
)) { |
213
|
|
|
throw new ScopeNotFoundException(sprintf( |
214
|
|
|
'Invalid inherited scope for %s object at %s scope, only ["%s"] supported, "%s" given.', |
215
|
|
|
get_class($object), |
216
|
|
|
$scope, |
217
|
|
|
implode(', ', array_keys($scopes)), |
218
|
|
|
$inheritedScope |
219
|
|
|
)); |
220
|
|
|
} |
221
|
|
|
|
222
|
6 |
|
array_unshift($stack, $scopes[$inheritedScope]); |
223
|
6 |
|
continue; |
224
|
|
|
} |
225
|
|
|
|
226
|
10 |
|
$fields[] = $fieldConfig; |
227
|
5 |
|
} |
228
|
10 |
|
} while (!empty($stack)); |
229
|
|
|
|
230
|
|
|
// begin normalization |
231
|
10 |
|
$data = array(); |
232
|
10 |
|
foreach ($fields as $field) { |
233
|
|
|
// optionnal field detection |
234
|
10 |
|
$optionnal = false; |
235
|
10 |
|
if (strpos($field, '?') !== false) { |
236
|
4 |
|
$field = str_replace('?', '', $field); |
237
|
4 |
|
$optionnal = true; |
238
|
2 |
|
} |
239
|
|
|
|
240
|
|
|
// external scopes : first in, last in |
241
|
10 |
|
$subScope = 'default'; |
242
|
10 |
|
if (strpos($field, '@') !== false) { |
243
|
4 |
|
list($field, $subScope) = explode('@', $field); |
244
|
2 |
|
} |
245
|
10 |
|
if (isset($data[$field])) { |
246
|
2 |
|
continue; |
247
|
|
|
} |
248
|
|
|
|
249
|
10 |
|
$value = $this->normalize( |
250
|
10 |
|
$read($field, $this->propertyAccessor), |
251
|
|
|
$subScope |
252
|
5 |
|
); |
253
|
|
|
|
254
|
|
|
// nullable ? |
255
|
10 |
|
if (!(is_null($value) && $optionnal)) { |
256
|
10 |
|
$data[$field] = $value; |
257
|
5 |
|
} |
258
|
5 |
|
} |
259
|
|
|
|
260
|
10 |
|
return $data; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* Create and return a Closure available to write an object property through a property path or a private property. |
265
|
|
|
* |
266
|
|
|
* @return \Closure |
267
|
|
|
*/ |
268
|
|
View Code Duplication |
private function createWrittingDelegate() |
|
|
|
|
269
|
|
|
{ |
270
|
18 |
|
return $this->writeDelegate ?: $this->writeDelegate = function (PropertyPathInterface $property, $value, PropertyAccessor $propertyAccessor) { |
271
|
9 |
|
switch (true) { |
272
|
|
|
|
273
|
|
|
// Public property / accessor case |
274
|
18 |
|
case $propertyAccessor->isWritable($this, $property) : |
|
|
|
|
275
|
16 |
|
return $propertyAccessor->setValue($this, $property, $value); |
276
|
|
|
|
277
|
|
|
// Private property / StdClass |
278
|
4 |
|
case property_exists($this, $property) || $this instanceof \StdClass : |
|
|
|
|
279
|
2 |
|
return $this->$property = $value; |
280
|
|
|
} |
281
|
|
|
|
282
|
2 |
|
throw new InvalidScopeException(sprintf( |
283
|
2 |
|
'Unable to set "%s" property into a "%s" object, any existing property path to write it in.', |
284
|
1 |
|
$property, |
285
|
1 |
|
get_class($this) |
286
|
1 |
|
)); |
287
|
18 |
|
}; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* Denormalize given object data into given normalizable object or class |
292
|
|
|
* If class given, normalizer will try to inject data into constructor if class is not a NormalizableInterface. |
293
|
|
|
* |
294
|
|
|
* @param mixed $data |
295
|
|
|
* @param object|string $normalizable normalizable object to denormalize in or an object class name |
296
|
|
|
* |
297
|
|
|
* @return NormalizableInterface |
298
|
|
|
*/ |
299
|
20 |
|
public function denormalize($data, $normalizable) |
300
|
|
|
{ |
301
|
20 |
|
$class = is_string($normalizable) ? |
302
|
12 |
|
$normalizable : ( |
303
|
8 |
|
$normalizable instanceof \ReflectionClass ? |
304
|
11 |
|
$normalizable->name : |
305
|
18 |
|
get_class($normalizable) |
306
|
|
|
) |
307
|
10 |
|
; |
308
|
20 |
|
$reflection = isset(self::$reflectionPool[$class]) ? |
309
|
17 |
|
self::$reflectionPool[$class] : |
310
|
10 |
|
self::$reflectionPool[$class] = $normalizable instanceof \ReflectionClass ? |
311
|
7 |
|
$normalizable : |
312
|
13 |
|
new \ReflectionClass($class) |
313
|
10 |
|
; |
314
|
|
|
|
315
|
20 |
|
$object = $normalizable; |
316
|
|
|
|
317
|
|
|
// Already got a denormalized object ? |
318
|
20 |
|
if (is_object($data) && is_a($data, $class)) { |
319
|
|
|
return $data; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
// Got reflection ? so build a new object |
323
|
20 |
|
if (is_string($object) || $object instanceof \ReflectionClass) { |
324
|
10 |
|
if (empty($data)) { // no data ? no worries ! |
325
|
|
|
return $reflection->newInstance(); |
326
|
|
|
} |
327
|
|
|
|
328
|
10 |
|
$arguments = array(); |
329
|
|
|
|
330
|
|
|
// Construct with parameters ? we will try to hydrate arguments from their names |
331
|
10 |
|
if ($reflection->hasMethod('__construct') |
332
|
10 |
|
&& count($parameters = $reflection->getMethod('__construct')->getParameters()) |
333
|
5 |
|
) { |
334
|
|
|
// String as items cases like \DateTime |
335
|
6 |
|
if (!is_array($data)) { |
336
|
4 |
|
$arguments = array($data); |
337
|
4 |
|
unset($data); |
338
|
2 |
|
} else { |
339
|
|
|
// Hydrate constructor args from data keys |
340
|
2 |
|
foreach ($parameters as $parameter) { |
341
|
2 |
|
$argKey = $this->inflector->snakelize($parameter->getName()); |
342
|
2 |
|
if (isset($data[$argKey])) { |
343
|
2 |
|
$arguments[] = $parameter->getClass() ? |
344
|
1 |
|
$this->normalize($data[$argKey], $parameter->getClass()) : |
345
|
2 |
|
$data[$argKey] |
346
|
|
|
; |
347
|
2 |
|
unset($data[$argKey]); |
348
|
|
|
|
349
|
2 |
|
continue; |
350
|
|
|
} |
351
|
|
|
|
352
|
2 |
|
$arguments[] = $parameter->isOptional() ? |
353
|
2 |
|
$parameter->getDefaultValue() : |
354
|
1 |
|
null |
355
|
|
|
; |
356
|
1 |
|
} |
357
|
|
|
} |
358
|
3 |
|
} |
359
|
|
|
|
360
|
10 |
|
$object = empty($arguments) ? |
361
|
7 |
|
$reflection->newInstance() : |
362
|
10 |
|
$reflection->newInstanceArgs($arguments); |
363
|
5 |
|
} |
364
|
|
|
|
365
|
|
|
// BAD ! |
366
|
20 |
|
if ($object instanceof EntityCollection) { |
367
|
|
|
return $object->denormalize($data); |
368
|
|
|
} |
369
|
|
|
|
370
|
20 |
|
if (empty($data)) { |
371
|
4 |
|
return $object; |
372
|
|
|
} |
373
|
|
|
|
374
|
18 |
|
$write = \Closure::bind( |
375
|
18 |
|
$this->createWrittingDelegate(), |
376
|
9 |
|
$object, |
377
|
9 |
|
get_class($object) |
378
|
9 |
|
); |
379
|
|
|
|
380
|
18 |
|
foreach ($data as $property => $value) { |
|
|
|
|
381
|
|
|
// Instanciate propertyPath before usage, to improve performance. |
382
|
18 |
|
if (!isset($this->propertiesPathPool[$property])) { |
383
|
18 |
|
$this->propertiesPathPool[$property] = new PropertyPath($property); |
384
|
9 |
|
} |
385
|
18 |
|
$propertyPath = $this->propertiesPathPool[$property]; |
386
|
|
|
|
387
|
|
|
// simple case : access property |
388
|
18 |
|
if (!$reflection->hasMethod($setter = sprintf('set%s', ucfirst($property)))) { |
389
|
8 |
|
$write($propertyPath, $value, $this->propertyAccessor); |
390
|
6 |
|
continue; |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
// extract setter class from type hinting |
394
|
16 |
|
$reflectionMethod = $reflection->getMethod($setter); |
395
|
16 |
|
$parameters = $reflectionMethod->getParameters(); |
396
|
16 |
|
$setParameter = $parameters[0]; |
397
|
|
|
|
398
|
|
|
// scalar or array ? |
|
|
|
|
399
|
16 |
|
if (!$setParameter->getClass() || $setParameter->isArray()) { |
400
|
16 |
|
$write($propertyPath, $value, $this->propertyAccessor); |
401
|
|
|
|
402
|
16 |
|
continue; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
// nullable object ? |
406
|
6 |
|
if (empty($value)) { |
407
|
|
|
if ($setParameter->allowsNull()) { |
408
|
|
|
$write($propertyPath, null, $this->propertyAccessor); |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
continue; |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
// callable ? |
415
|
6 |
|
if (is_callable($value)) { |
416
|
|
|
if ($setParameter->isCallable()) { |
417
|
|
|
$write($propertyPath, $value, $this->propertyAccessor); |
418
|
|
|
} |
419
|
|
|
} |
420
|
|
|
|
421
|
6 |
|
$write( |
422
|
3 |
|
$propertyPath, |
423
|
6 |
|
$this->denormalize($value, $setParameter->getClass()), |
424
|
6 |
|
$this->propertyAccessor |
425
|
3 |
|
); |
426
|
8 |
|
} |
427
|
|
|
|
428
|
16 |
|
return $object; |
429
|
|
|
} |
430
|
|
|
} |
431
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.