25d6ff6a9d64e6cbf976beda2c7efd4fe531c6fb
[yaffs-website] / vendor / symfony / serializer / Normalizer / AbstractObjectNormalizer.php
1 <?php
2
3 /*
4  * This file is part of the Symfony package.
5  *
6  * (c) Fabien Potencier <fabien@symfony.com>
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Symfony\Component\Serializer\Normalizer;
13
14 use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
15 use Symfony\Component\Serializer\Encoder\JsonEncoder;
16 use Symfony\Component\Serializer\Exception\ExtraAttributesException;
17 use Symfony\Component\Serializer\Exception\LogicException;
18 use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
19 use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
20 use Symfony\Component\PropertyInfo\Type;
21 use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
22 use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
23 use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
24
25 /**
26  * Base class for a normalizer dealing with objects.
27  *
28  * @author Kévin Dunglas <dunglas@gmail.com>
29  */
30 abstract class AbstractObjectNormalizer extends AbstractNormalizer
31 {
32     const ENABLE_MAX_DEPTH = 'enable_max_depth';
33     const DEPTH_KEY_PATTERN = 'depth_%s::%s';
34     const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
35
36     private $propertyTypeExtractor;
37     private $attributesCache = array();
38     private $cache = array();
39
40     public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
41     {
42         parent::__construct($classMetadataFactory, $nameConverter);
43
44         $this->propertyTypeExtractor = $propertyTypeExtractor;
45     }
46
47     /**
48      * {@inheritdoc}
49      */
50     public function supportsNormalization($data, $format = null)
51     {
52         return \is_object($data) && !$data instanceof \Traversable;
53     }
54
55     /**
56      * {@inheritdoc}
57      */
58     public function normalize($object, $format = null, array $context = array())
59     {
60         if (!isset($context['cache_key'])) {
61             $context['cache_key'] = $this->getCacheKey($format, $context);
62         }
63
64         if ($this->isCircularReference($object, $context)) {
65             return $this->handleCircularReference($object);
66         }
67
68         $data = array();
69         $stack = array();
70         $attributes = $this->getAttributes($object, $format, $context);
71         $class = get_class($object);
72         $attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
73
74         foreach ($attributes as $attribute) {
75             if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) {
76                 continue;
77             }
78
79             $attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
80
81             if (isset($this->callbacks[$attribute])) {
82                 $attributeValue = call_user_func($this->callbacks[$attribute], $attributeValue);
83             }
84
85             if (null !== $attributeValue && !is_scalar($attributeValue)) {
86                 $stack[$attribute] = $attributeValue;
87             }
88
89             $data = $this->updateData($data, $attribute, $attributeValue);
90         }
91
92         foreach ($stack as $attribute => $attributeValue) {
93             if (!$this->serializer instanceof NormalizerInterface) {
94                 throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer', $attribute));
95             }
96
97             $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute)));
98         }
99
100         return $data;
101     }
102
103     /**
104      * Gets and caches attributes for the given object, format and context.
105      *
106      * @param object      $object
107      * @param string|null $format
108      * @param array       $context
109      *
110      * @return string[]
111      */
112     protected function getAttributes($object, $format = null, array $context)
113     {
114         $class = get_class($object);
115         $key = $class.'-'.$context['cache_key'];
116
117         if (isset($this->attributesCache[$key])) {
118             return $this->attributesCache[$key];
119         }
120
121         $allowedAttributes = $this->getAllowedAttributes($object, $context, true);
122
123         if (false !== $allowedAttributes) {
124             if ($context['cache_key']) {
125                 $this->attributesCache[$key] = $allowedAttributes;
126             }
127
128             return $allowedAttributes;
129         }
130
131         if (isset($context['attributes'])) {
132             return $this->extractAttributes($object, $format, $context);
133         }
134
135         if (isset($this->attributesCache[$class])) {
136             return $this->attributesCache[$class];
137         }
138
139         return $this->attributesCache[$class] = $this->extractAttributes($object, $format, $context);
140     }
141
142     /**
143      * Extracts attributes to normalize from the class of the given object, format and context.
144      *
145      * @param object      $object
146      * @param string|null $format
147      * @param array       $context
148      *
149      * @return string[]
150      */
151     abstract protected function extractAttributes($object, $format = null, array $context = array());
152
153     /**
154      * Gets the attribute value.
155      *
156      * @param object      $object
157      * @param string      $attribute
158      * @param string|null $format
159      * @param array       $context
160      *
161      * @return mixed
162      */
163     abstract protected function getAttributeValue($object, $attribute, $format = null, array $context = array());
164
165     /**
166      * {@inheritdoc}
167      */
168     public function supportsDenormalization($data, $type, $format = null)
169     {
170         return isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = class_exists($type);
171     }
172
173     /**
174      * {@inheritdoc}
175      */
176     public function denormalize($data, $class, $format = null, array $context = array())
177     {
178         if (!isset($context['cache_key'])) {
179             $context['cache_key'] = $this->getCacheKey($format, $context);
180         }
181
182         $allowedAttributes = $this->getAllowedAttributes($class, $context, true);
183         $normalizedData = $this->prepareForDenormalization($data);
184         $extraAttributes = array();
185
186         $reflectionClass = new \ReflectionClass($class);
187         $object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format);
188
189         foreach ($normalizedData as $attribute => $value) {
190             if ($this->nameConverter) {
191                 $attribute = $this->nameConverter->denormalize($attribute);
192             }
193
194             if ((false !== $allowedAttributes && !in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($class, $attribute, $format, $context)) {
195                 if (isset($context[self::ALLOW_EXTRA_ATTRIBUTES]) && !$context[self::ALLOW_EXTRA_ATTRIBUTES]) {
196                     $extraAttributes[] = $attribute;
197                 }
198
199                 continue;
200             }
201
202             $value = $this->validateAndDenormalize($class, $attribute, $value, $format, $context);
203             try {
204                 $this->setAttributeValue($object, $attribute, $value, $format, $context);
205             } catch (InvalidArgumentException $e) {
206                 throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
207             }
208         }
209
210         if (!empty($extraAttributes)) {
211             throw new ExtraAttributesException($extraAttributes);
212         }
213
214         return $object;
215     }
216
217     /**
218      * Sets attribute value.
219      *
220      * @param object      $object
221      * @param string      $attribute
222      * @param mixed       $value
223      * @param string|null $format
224      * @param array       $context
225      */
226     abstract protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array());
227
228     /**
229      * Validates the submitted data and denormalizes it.
230      *
231      * @param string      $currentClass
232      * @param string      $attribute
233      * @param mixed       $data
234      * @param string|null $format
235      * @param array       $context
236      *
237      * @return mixed
238      *
239      * @throws NotNormalizableValueException
240      * @throws LogicException
241      */
242     private function validateAndDenormalize($currentClass, $attribute, $data, $format, array $context)
243     {
244         if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
245             return $data;
246         }
247
248         $expectedTypes = array();
249         foreach ($types as $type) {
250             if (null === $data && $type->isNullable()) {
251                 return;
252             }
253
254             if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
255                 $builtinType = Type::BUILTIN_TYPE_OBJECT;
256                 $class = $collectionValueType->getClassName().'[]';
257
258                 if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
259                     $context['key_type'] = $collectionKeyType;
260                 }
261             } else {
262                 $builtinType = $type->getBuiltinType();
263                 $class = $type->getClassName();
264             }
265
266             $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
267
268             if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
269                 if (!$this->serializer instanceof DenormalizerInterface) {
270                     throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer', $attribute, $class));
271                 }
272
273                 $childContext = $this->createChildContext($context, $attribute);
274                 if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) {
275                     return $this->serializer->denormalize($data, $class, $format, $childContext);
276                 }
277             }
278
279             // JSON only has a Number type corresponding to both int and float PHP types.
280             // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
281             // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
282             // PHP's json_decode automatically converts Numbers without a decimal part to integers.
283             // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
284             // a float is expected.
285             if (Type::BUILTIN_TYPE_FLOAT === $builtinType && is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) {
286                 return (float) $data;
287             }
288
289             if (call_user_func('is_'.$builtinType, $data)) {
290                 return $data;
291             }
292         }
293
294         if (!empty($context[self::DISABLE_TYPE_ENFORCEMENT])) {
295             return $data;
296         }
297
298         throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), gettype($data)));
299     }
300
301     /**
302      * Sets an attribute and apply the name converter if necessary.
303      *
304      * @param array  $data
305      * @param string $attribute
306      * @param mixed  $attributeValue
307      *
308      * @return array
309      */
310     private function updateData(array $data, $attribute, $attributeValue)
311     {
312         if ($this->nameConverter) {
313             $attribute = $this->nameConverter->normalize($attribute);
314         }
315
316         $data[$attribute] = $attributeValue;
317
318         return $data;
319     }
320
321     /**
322      * Is the max depth reached for the given attribute?
323      *
324      * @param AttributeMetadataInterface[] $attributesMetadata
325      * @param string                       $class
326      * @param string                       $attribute
327      * @param array                        $context
328      *
329      * @return bool
330      */
331     private function isMaxDepthReached(array $attributesMetadata, $class, $attribute, array &$context)
332     {
333         if (
334             !isset($context[static::ENABLE_MAX_DEPTH]) ||
335             !isset($attributesMetadata[$attribute]) ||
336             null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
337         ) {
338             return false;
339         }
340
341         $key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute);
342         if (!isset($context[$key])) {
343             $context[$key] = 1;
344
345             return false;
346         }
347
348         if ($context[$key] === $maxDepth) {
349             return true;
350         }
351
352         ++$context[$key];
353
354         return false;
355     }
356
357     /**
358      * Gets the cache key to use.
359      *
360      * @param string|null $format
361      * @param array       $context
362      *
363      * @return bool|string
364      */
365     private function getCacheKey($format, array $context)
366     {
367         try {
368             return md5($format.serialize($context));
369         } catch (\Exception $exception) {
370             // The context cannot be serialized, skip the cache
371             return false;
372         }
373     }
374 }