Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 /*
|
Chris@0
|
4 * This file is part of the Symfony package.
|
Chris@0
|
5 *
|
Chris@0
|
6 * (c) Fabien Potencier <fabien@symfony.com>
|
Chris@0
|
7 *
|
Chris@0
|
8 * For the full copyright and license information, please view the LICENSE
|
Chris@0
|
9 * file that was distributed with this source code.
|
Chris@0
|
10 */
|
Chris@0
|
11
|
Chris@0
|
12 namespace Symfony\Component\Serializer\Normalizer;
|
Chris@0
|
13
|
Chris@0
|
14 use Symfony\Component\Serializer\Exception\CircularReferenceException;
|
Chris@0
|
15 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
|
Chris@0
|
16 use Symfony\Component\Serializer\Exception\LogicException;
|
Chris@0
|
17 use Symfony\Component\Serializer\Exception\RuntimeException;
|
Chris@17
|
18 use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
|
Chris@0
|
19 use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
|
Chris@0
|
20 use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
|
Chris@0
|
21 use Symfony\Component\Serializer\SerializerAwareInterface;
|
Chris@0
|
22
|
Chris@0
|
23 /**
|
Chris@0
|
24 * Normalizer implementation.
|
Chris@0
|
25 *
|
Chris@0
|
26 * @author Kévin Dunglas <dunglas@gmail.com>
|
Chris@0
|
27 */
|
Chris@0
|
28 abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
|
Chris@0
|
29 {
|
Chris@14
|
30 use ObjectToPopulateTrait;
|
Chris@14
|
31
|
Chris@0
|
32 const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
|
Chris@0
|
33 const OBJECT_TO_POPULATE = 'object_to_populate';
|
Chris@0
|
34 const GROUPS = 'groups';
|
Chris@14
|
35 const ATTRIBUTES = 'attributes';
|
Chris@14
|
36 const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
|
Chris@0
|
37
|
Chris@0
|
38 /**
|
Chris@0
|
39 * @var int
|
Chris@0
|
40 */
|
Chris@0
|
41 protected $circularReferenceLimit = 1;
|
Chris@0
|
42
|
Chris@0
|
43 /**
|
Chris@0
|
44 * @var callable
|
Chris@0
|
45 */
|
Chris@0
|
46 protected $circularReferenceHandler;
|
Chris@0
|
47
|
Chris@0
|
48 /**
|
Chris@0
|
49 * @var ClassMetadataFactoryInterface|null
|
Chris@0
|
50 */
|
Chris@0
|
51 protected $classMetadataFactory;
|
Chris@0
|
52
|
Chris@0
|
53 /**
|
Chris@0
|
54 * @var NameConverterInterface|null
|
Chris@0
|
55 */
|
Chris@0
|
56 protected $nameConverter;
|
Chris@0
|
57
|
Chris@0
|
58 /**
|
Chris@0
|
59 * @var array
|
Chris@0
|
60 */
|
Chris@17
|
61 protected $callbacks = [];
|
Chris@0
|
62
|
Chris@0
|
63 /**
|
Chris@0
|
64 * @var array
|
Chris@0
|
65 */
|
Chris@17
|
66 protected $ignoredAttributes = [];
|
Chris@0
|
67
|
Chris@0
|
68 /**
|
Chris@0
|
69 * @var array
|
Chris@0
|
70 */
|
Chris@17
|
71 protected $camelizedAttributes = [];
|
Chris@0
|
72
|
Chris@0
|
73 /**
|
Chris@0
|
74 * Sets the {@link ClassMetadataFactoryInterface} to use.
|
Chris@0
|
75 */
|
Chris@0
|
76 public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null)
|
Chris@0
|
77 {
|
Chris@0
|
78 $this->classMetadataFactory = $classMetadataFactory;
|
Chris@0
|
79 $this->nameConverter = $nameConverter;
|
Chris@0
|
80 }
|
Chris@0
|
81
|
Chris@0
|
82 /**
|
Chris@0
|
83 * Set circular reference limit.
|
Chris@0
|
84 *
|
Chris@14
|
85 * @param int $circularReferenceLimit Limit of iterations for the same object
|
Chris@0
|
86 *
|
Chris@0
|
87 * @return self
|
Chris@0
|
88 */
|
Chris@0
|
89 public function setCircularReferenceLimit($circularReferenceLimit)
|
Chris@0
|
90 {
|
Chris@0
|
91 $this->circularReferenceLimit = $circularReferenceLimit;
|
Chris@0
|
92
|
Chris@0
|
93 return $this;
|
Chris@0
|
94 }
|
Chris@0
|
95
|
Chris@0
|
96 /**
|
Chris@0
|
97 * Set circular reference handler.
|
Chris@0
|
98 *
|
Chris@0
|
99 * @param callable $circularReferenceHandler
|
Chris@0
|
100 *
|
Chris@0
|
101 * @return self
|
Chris@0
|
102 */
|
Chris@0
|
103 public function setCircularReferenceHandler(callable $circularReferenceHandler)
|
Chris@0
|
104 {
|
Chris@0
|
105 $this->circularReferenceHandler = $circularReferenceHandler;
|
Chris@0
|
106
|
Chris@0
|
107 return $this;
|
Chris@0
|
108 }
|
Chris@0
|
109
|
Chris@0
|
110 /**
|
Chris@0
|
111 * Set normalization callbacks.
|
Chris@0
|
112 *
|
Chris@14
|
113 * @param callable[] $callbacks Help normalize the result
|
Chris@0
|
114 *
|
Chris@0
|
115 * @return self
|
Chris@0
|
116 *
|
Chris@0
|
117 * @throws InvalidArgumentException if a non-callable callback is set
|
Chris@0
|
118 */
|
Chris@0
|
119 public function setCallbacks(array $callbacks)
|
Chris@0
|
120 {
|
Chris@0
|
121 foreach ($callbacks as $attribute => $callback) {
|
Chris@14
|
122 if (!\is_callable($callback)) {
|
Chris@17
|
123 throw new InvalidArgumentException(sprintf('The given callback for attribute "%s" is not callable.', $attribute));
|
Chris@0
|
124 }
|
Chris@0
|
125 }
|
Chris@0
|
126 $this->callbacks = $callbacks;
|
Chris@0
|
127
|
Chris@0
|
128 return $this;
|
Chris@0
|
129 }
|
Chris@0
|
130
|
Chris@0
|
131 /**
|
Chris@0
|
132 * Set ignored attributes for normalization and denormalization.
|
Chris@0
|
133 *
|
Chris@0
|
134 * @return self
|
Chris@0
|
135 */
|
Chris@0
|
136 public function setIgnoredAttributes(array $ignoredAttributes)
|
Chris@0
|
137 {
|
Chris@0
|
138 $this->ignoredAttributes = $ignoredAttributes;
|
Chris@0
|
139
|
Chris@0
|
140 return $this;
|
Chris@0
|
141 }
|
Chris@0
|
142
|
Chris@0
|
143 /**
|
Chris@0
|
144 * Detects if the configured circular reference limit is reached.
|
Chris@0
|
145 *
|
Chris@0
|
146 * @param object $object
|
Chris@0
|
147 * @param array $context
|
Chris@0
|
148 *
|
Chris@0
|
149 * @return bool
|
Chris@0
|
150 *
|
Chris@0
|
151 * @throws CircularReferenceException
|
Chris@0
|
152 */
|
Chris@0
|
153 protected function isCircularReference($object, &$context)
|
Chris@0
|
154 {
|
Chris@0
|
155 $objectHash = spl_object_hash($object);
|
Chris@0
|
156
|
Chris@0
|
157 if (isset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash])) {
|
Chris@0
|
158 if ($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] >= $this->circularReferenceLimit) {
|
Chris@0
|
159 unset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]);
|
Chris@0
|
160
|
Chris@0
|
161 return true;
|
Chris@0
|
162 }
|
Chris@0
|
163
|
Chris@0
|
164 ++$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash];
|
Chris@0
|
165 } else {
|
Chris@0
|
166 $context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] = 1;
|
Chris@0
|
167 }
|
Chris@0
|
168
|
Chris@0
|
169 return false;
|
Chris@0
|
170 }
|
Chris@0
|
171
|
Chris@0
|
172 /**
|
Chris@0
|
173 * Handles a circular reference.
|
Chris@0
|
174 *
|
Chris@0
|
175 * If a circular reference handler is set, it will be called. Otherwise, a
|
Chris@0
|
176 * {@class CircularReferenceException} will be thrown.
|
Chris@0
|
177 *
|
Chris@0
|
178 * @param object $object
|
Chris@0
|
179 *
|
Chris@0
|
180 * @return mixed
|
Chris@0
|
181 *
|
Chris@0
|
182 * @throws CircularReferenceException
|
Chris@0
|
183 */
|
Chris@0
|
184 protected function handleCircularReference($object)
|
Chris@0
|
185 {
|
Chris@0
|
186 if ($this->circularReferenceHandler) {
|
Chris@14
|
187 return \call_user_func($this->circularReferenceHandler, $object);
|
Chris@0
|
188 }
|
Chris@0
|
189
|
Chris@14
|
190 throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d)', \get_class($object), $this->circularReferenceLimit));
|
Chris@0
|
191 }
|
Chris@0
|
192
|
Chris@0
|
193 /**
|
Chris@0
|
194 * Gets attributes to normalize using groups.
|
Chris@0
|
195 *
|
Chris@0
|
196 * @param string|object $classOrObject
|
Chris@0
|
197 * @param array $context
|
Chris@0
|
198 * @param bool $attributesAsString If false, return an array of {@link AttributeMetadataInterface}
|
Chris@0
|
199 *
|
Chris@16
|
200 * @throws LogicException if the 'allow_extra_attributes' context variable is false and no class metadata factory is provided
|
Chris@16
|
201 *
|
Chris@0
|
202 * @return string[]|AttributeMetadataInterface[]|bool
|
Chris@0
|
203 */
|
Chris@0
|
204 protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
|
Chris@0
|
205 {
|
Chris@14
|
206 if (!$this->classMetadataFactory) {
|
Chris@16
|
207 if (isset($context[static::ALLOW_EXTRA_ATTRIBUTES]) && !$context[static::ALLOW_EXTRA_ATTRIBUTES]) {
|
Chris@16
|
208 throw new LogicException(sprintf('A class metadata factory must be provided in the constructor when setting "%s" to false.', static::ALLOW_EXTRA_ATTRIBUTES));
|
Chris@16
|
209 }
|
Chris@16
|
210
|
Chris@14
|
211 return false;
|
Chris@14
|
212 }
|
Chris@14
|
213
|
Chris@14
|
214 $groups = false;
|
Chris@14
|
215 if (isset($context[static::GROUPS]) && \is_array($context[static::GROUPS])) {
|
Chris@14
|
216 $groups = $context[static::GROUPS];
|
Chris@14
|
217 } elseif (!isset($context[static::ALLOW_EXTRA_ATTRIBUTES]) || $context[static::ALLOW_EXTRA_ATTRIBUTES]) {
|
Chris@0
|
218 return false;
|
Chris@0
|
219 }
|
Chris@0
|
220
|
Chris@17
|
221 $allowedAttributes = [];
|
Chris@0
|
222 foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) {
|
Chris@0
|
223 $name = $attributeMetadata->getName();
|
Chris@0
|
224
|
Chris@0
|
225 if (
|
Chris@14
|
226 (false === $groups || array_intersect($attributeMetadata->getGroups(), $groups)) &&
|
Chris@0
|
227 $this->isAllowedAttribute($classOrObject, $name, null, $context)
|
Chris@0
|
228 ) {
|
Chris@0
|
229 $allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata;
|
Chris@0
|
230 }
|
Chris@0
|
231 }
|
Chris@0
|
232
|
Chris@0
|
233 return $allowedAttributes;
|
Chris@0
|
234 }
|
Chris@0
|
235
|
Chris@0
|
236 /**
|
Chris@0
|
237 * Is this attribute allowed?
|
Chris@0
|
238 *
|
Chris@0
|
239 * @param object|string $classOrObject
|
Chris@0
|
240 * @param string $attribute
|
Chris@0
|
241 * @param string|null $format
|
Chris@0
|
242 * @param array $context
|
Chris@0
|
243 *
|
Chris@0
|
244 * @return bool
|
Chris@0
|
245 */
|
Chris@17
|
246 protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = [])
|
Chris@0
|
247 {
|
Chris@17
|
248 if (\in_array($attribute, $this->ignoredAttributes)) {
|
Chris@14
|
249 return false;
|
Chris@14
|
250 }
|
Chris@14
|
251
|
Chris@14
|
252 if (isset($context[self::ATTRIBUTES][$attribute])) {
|
Chris@14
|
253 // Nested attributes
|
Chris@14
|
254 return true;
|
Chris@14
|
255 }
|
Chris@14
|
256
|
Chris@17
|
257 if (isset($context[self::ATTRIBUTES]) && \is_array($context[self::ATTRIBUTES])) {
|
Chris@17
|
258 return \in_array($attribute, $context[self::ATTRIBUTES], true);
|
Chris@14
|
259 }
|
Chris@14
|
260
|
Chris@14
|
261 return true;
|
Chris@0
|
262 }
|
Chris@0
|
263
|
Chris@0
|
264 /**
|
Chris@0
|
265 * Normalizes the given data to an array. It's particularly useful during
|
Chris@0
|
266 * the denormalization process.
|
Chris@0
|
267 *
|
Chris@0
|
268 * @param object|array $data
|
Chris@0
|
269 *
|
Chris@0
|
270 * @return array
|
Chris@0
|
271 */
|
Chris@0
|
272 protected function prepareForDenormalization($data)
|
Chris@0
|
273 {
|
Chris@0
|
274 return (array) $data;
|
Chris@0
|
275 }
|
Chris@0
|
276
|
Chris@0
|
277 /**
|
Chris@0
|
278 * Returns the method to use to construct an object. This method must be either
|
Chris@0
|
279 * the object constructor or static.
|
Chris@0
|
280 *
|
Chris@0
|
281 * @param array $data
|
Chris@0
|
282 * @param string $class
|
Chris@0
|
283 * @param array $context
|
Chris@0
|
284 * @param \ReflectionClass $reflectionClass
|
Chris@0
|
285 * @param array|bool $allowedAttributes
|
Chris@0
|
286 *
|
Chris@0
|
287 * @return \ReflectionMethod|null
|
Chris@0
|
288 */
|
Chris@0
|
289 protected function getConstructor(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes)
|
Chris@0
|
290 {
|
Chris@0
|
291 return $reflectionClass->getConstructor();
|
Chris@0
|
292 }
|
Chris@0
|
293
|
Chris@0
|
294 /**
|
Chris@0
|
295 * Instantiates an object using constructor parameters when needed.
|
Chris@0
|
296 *
|
Chris@0
|
297 * This method also allows to denormalize data into an existing object if
|
Chris@0
|
298 * it is present in the context with the object_to_populate. This object
|
Chris@0
|
299 * is removed from the context before being returned to avoid side effects
|
Chris@0
|
300 * when recursively normalizing an object graph.
|
Chris@0
|
301 *
|
Chris@0
|
302 * @param array $data
|
Chris@0
|
303 * @param string $class
|
Chris@0
|
304 * @param array $context
|
Chris@0
|
305 * @param \ReflectionClass $reflectionClass
|
Chris@0
|
306 * @param array|bool $allowedAttributes
|
Chris@0
|
307 * @param string|null $format
|
Chris@0
|
308 *
|
Chris@0
|
309 * @return object
|
Chris@0
|
310 *
|
Chris@0
|
311 * @throws RuntimeException
|
Chris@0
|
312 */
|
Chris@14
|
313 protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes/*, string $format = null*/)
|
Chris@0
|
314 {
|
Chris@14
|
315 if (\func_num_args() >= 6) {
|
Chris@14
|
316 $format = \func_get_arg(5);
|
Chris@0
|
317 } else {
|
Chris@14
|
318 if (__CLASS__ !== \get_class($this)) {
|
Chris@0
|
319 $r = new \ReflectionMethod($this, __FUNCTION__);
|
Chris@0
|
320 if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
|
Chris@17
|
321 @trigger_error(sprintf('Method %s::%s() will have a 6th `string $format = null` argument in version 4.0. Not defining it is deprecated since Symfony 3.2.', \get_class($this), __FUNCTION__), E_USER_DEPRECATED);
|
Chris@0
|
322 }
|
Chris@0
|
323 }
|
Chris@0
|
324
|
Chris@0
|
325 $format = null;
|
Chris@0
|
326 }
|
Chris@0
|
327
|
Chris@14
|
328 if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
|
Chris@0
|
329 unset($context[static::OBJECT_TO_POPULATE]);
|
Chris@0
|
330
|
Chris@0
|
331 return $object;
|
Chris@0
|
332 }
|
Chris@18
|
333 // clean up even if no match
|
Chris@18
|
334 unset($context[static::OBJECT_TO_POPULATE]);
|
Chris@0
|
335
|
Chris@0
|
336 $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
|
Chris@0
|
337 if ($constructor) {
|
Chris@18
|
338 if (true !== $constructor->isPublic()) {
|
Chris@18
|
339 return $reflectionClass->newInstanceWithoutConstructor();
|
Chris@18
|
340 }
|
Chris@18
|
341
|
Chris@0
|
342 $constructorParameters = $constructor->getParameters();
|
Chris@0
|
343
|
Chris@17
|
344 $params = [];
|
Chris@0
|
345 foreach ($constructorParameters as $constructorParameter) {
|
Chris@0
|
346 $paramName = $constructorParameter->name;
|
Chris@0
|
347 $key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName;
|
Chris@0
|
348
|
Chris@14
|
349 $allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes);
|
Chris@14
|
350 $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
|
Chris@0
|
351 if (method_exists($constructorParameter, 'isVariadic') && $constructorParameter->isVariadic()) {
|
Chris@18
|
352 if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
|
Chris@14
|
353 if (!\is_array($data[$paramName])) {
|
Chris@0
|
354 throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.', $class, $constructorParameter->name));
|
Chris@0
|
355 }
|
Chris@0
|
356
|
Chris@0
|
357 $params = array_merge($params, $data[$paramName]);
|
Chris@0
|
358 }
|
Chris@18
|
359 } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
|
Chris@0
|
360 $parameterData = $data[$key];
|
Chris@16
|
361 if (null === $parameterData && $constructorParameter->allowsNull()) {
|
Chris@16
|
362 $params[] = null;
|
Chris@16
|
363 // Don't run set for a parameter passed to the constructor
|
Chris@16
|
364 unset($data[$key]);
|
Chris@16
|
365 continue;
|
Chris@16
|
366 }
|
Chris@0
|
367
|
Chris@0
|
368 // Don't run set for a parameter passed to the constructor
|
Chris@17
|
369 $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format);
|
Chris@0
|
370 unset($data[$key]);
|
Chris@0
|
371 } elseif ($constructorParameter->isDefaultValueAvailable()) {
|
Chris@0
|
372 $params[] = $constructorParameter->getDefaultValue();
|
Chris@0
|
373 } else {
|
Chris@17
|
374 throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
|
Chris@0
|
375 }
|
Chris@0
|
376 }
|
Chris@0
|
377
|
Chris@0
|
378 if ($constructor->isConstructor()) {
|
Chris@0
|
379 return $reflectionClass->newInstanceArgs($params);
|
Chris@0
|
380 } else {
|
Chris@0
|
381 return $constructor->invokeArgs(null, $params);
|
Chris@0
|
382 }
|
Chris@0
|
383 }
|
Chris@0
|
384
|
Chris@0
|
385 return new $class();
|
Chris@0
|
386 }
|
Chris@14
|
387
|
Chris@14
|
388 /**
|
Chris@17
|
389 * @internal
|
Chris@17
|
390 */
|
Chris@17
|
391 protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null)
|
Chris@17
|
392 {
|
Chris@17
|
393 try {
|
Chris@17
|
394 if (null !== $parameter->getClass()) {
|
Chris@17
|
395 if (!$this->serializer instanceof DenormalizerInterface) {
|
Chris@17
|
396 throw new LogicException(sprintf('Cannot create an instance of %s from serialized data because the serializer inject in "%s" is not a denormalizer', $parameter->getClass(), static::class));
|
Chris@17
|
397 }
|
Chris@17
|
398 $parameterClass = $parameter->getClass()->getName();
|
Chris@17
|
399
|
Chris@18
|
400 return $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format));
|
Chris@17
|
401 }
|
Chris@17
|
402
|
Chris@17
|
403 return $parameterData;
|
Chris@17
|
404 } catch (\ReflectionException $e) {
|
Chris@17
|
405 throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e);
|
Chris@17
|
406 }
|
Chris@17
|
407 }
|
Chris@17
|
408
|
Chris@17
|
409 /**
|
Chris@18
|
410 * @param array $parentContext
|
Chris@18
|
411 * @param string $attribute Attribute name
|
Chris@18
|
412 * @param string|null $format
|
Chris@14
|
413 *
|
Chris@14
|
414 * @return array
|
Chris@14
|
415 *
|
Chris@14
|
416 * @internal
|
Chris@14
|
417 */
|
Chris@18
|
418 protected function createChildContext(array $parentContext, $attribute/*, string $format = null */)
|
Chris@14
|
419 {
|
Chris@14
|
420 if (isset($parentContext[self::ATTRIBUTES][$attribute])) {
|
Chris@14
|
421 $parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute];
|
Chris@14
|
422 } else {
|
Chris@14
|
423 unset($parentContext[self::ATTRIBUTES]);
|
Chris@14
|
424 }
|
Chris@14
|
425
|
Chris@14
|
426 return $parentContext;
|
Chris@14
|
427 }
|
Chris@0
|
428 }
|