4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Serializer\Normalizer;
14 use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
15 use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
16 use Symfony\Component\PropertyInfo\Type;
17 use Symfony\Component\Serializer\Encoder\JsonEncoder;
18 use Symfony\Component\Serializer\Exception\ExtraAttributesException;
19 use Symfony\Component\Serializer\Exception\LogicException;
20 use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
21 use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
22 use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
23 use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
26 * Base class for a normalizer dealing with objects.
28 * @author Kévin Dunglas <dunglas@gmail.com>
30 abstract class AbstractObjectNormalizer extends AbstractNormalizer
32 const ENABLE_MAX_DEPTH = 'enable_max_depth';
33 const DEPTH_KEY_PATTERN = 'depth_%s::%s';
34 const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
36 private $propertyTypeExtractor;
37 private $attributesCache = array();
38 private $cache = array();
40 public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
42 parent::__construct($classMetadataFactory, $nameConverter);
44 $this->propertyTypeExtractor = $propertyTypeExtractor;
50 public function supportsNormalization($data, $format = null)
52 return \is_object($data) && !$data instanceof \Traversable;
58 public function normalize($object, $format = null, array $context = array())
60 if (!isset($context['cache_key'])) {
61 $context['cache_key'] = $this->getCacheKey($format, $context);
64 if ($this->isCircularReference($object, $context)) {
65 return $this->handleCircularReference($object);
70 $attributes = $this->getAttributes($object, $format, $context);
71 $class = \get_class($object);
72 $attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
74 foreach ($attributes as $attribute) {
75 if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) {
79 $attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
81 if (isset($this->callbacks[$attribute])) {
82 $attributeValue = \call_user_func($this->callbacks[$attribute], $attributeValue);
85 if (null !== $attributeValue && !is_scalar($attributeValue)) {
86 $stack[$attribute] = $attributeValue;
89 $data = $this->updateData($data, $attribute, $attributeValue);
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));
97 $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute)));
104 * Gets and caches attributes for the given object, format and context.
106 * @param object $object
107 * @param string|null $format
108 * @param array $context
112 protected function getAttributes($object, $format = null, array $context)
114 $class = \get_class($object);
115 $key = $class.'-'.$context['cache_key'];
117 if (isset($this->attributesCache[$key])) {
118 return $this->attributesCache[$key];
121 $allowedAttributes = $this->getAllowedAttributes($object, $context, true);
123 if (false !== $allowedAttributes) {
124 if ($context['cache_key']) {
125 $this->attributesCache[$key] = $allowedAttributes;
128 return $allowedAttributes;
131 if (isset($context['attributes'])) {
132 return $this->extractAttributes($object, $format, $context);
135 if (isset($this->attributesCache[$class])) {
136 return $this->attributesCache[$class];
139 return $this->attributesCache[$class] = $this->extractAttributes($object, $format, $context);
143 * Extracts attributes to normalize from the class of the given object, format and context.
145 * @param object $object
146 * @param string|null $format
147 * @param array $context
151 abstract protected function extractAttributes($object, $format = null, array $context = array());
154 * Gets the attribute value.
156 * @param object $object
157 * @param string $attribute
158 * @param string|null $format
159 * @param array $context
163 abstract protected function getAttributeValue($object, $attribute, $format = null, array $context = array());
168 public function supportsDenormalization($data, $type, $format = null)
170 return isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = class_exists($type);
176 public function denormalize($data, $class, $format = null, array $context = array())
178 if (!isset($context['cache_key'])) {
179 $context['cache_key'] = $this->getCacheKey($format, $context);
182 $allowedAttributes = $this->getAllowedAttributes($class, $context, true);
183 $normalizedData = $this->prepareForDenormalization($data);
184 $extraAttributes = array();
186 $reflectionClass = new \ReflectionClass($class);
187 $object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format);
189 foreach ($normalizedData as $attribute => $value) {
190 if ($this->nameConverter) {
191 $attribute = $this->nameConverter->denormalize($attribute);
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;
202 $value = $this->validateAndDenormalize($class, $attribute, $value, $format, $context);
204 $this->setAttributeValue($object, $attribute, $value, $format, $context);
205 } catch (InvalidArgumentException $e) {
206 throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
210 if (!empty($extraAttributes)) {
211 throw new ExtraAttributesException($extraAttributes);
218 * Sets attribute value.
220 * @param object $object
221 * @param string $attribute
222 * @param mixed $value
223 * @param string|null $format
224 * @param array $context
226 abstract protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array());
229 * Validates the submitted data and denormalizes it.
231 * @param string $currentClass
232 * @param string $attribute
234 * @param string|null $format
235 * @param array $context
239 * @throws NotNormalizableValueException
240 * @throws LogicException
242 private function validateAndDenormalize($currentClass, $attribute, $data, $format, array $context)
244 if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
248 $expectedTypes = array();
249 foreach ($types as $type) {
250 if (null === $data && $type->isNullable()) {
254 if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
255 $builtinType = Type::BUILTIN_TYPE_OBJECT;
256 $class = $collectionValueType->getClassName().'[]';
258 // Fix a collection that contains the only one element
259 // This is special to xml format only
260 if ('xml' === $format && !\is_int(key($data))) {
261 $data = array($data);
264 if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
265 $context['key_type'] = $collectionKeyType;
268 $builtinType = $type->getBuiltinType();
269 $class = $type->getClassName();
272 $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
274 if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
275 if (!$this->serializer instanceof DenormalizerInterface) {
276 throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer', $attribute, $class));
279 $childContext = $this->createChildContext($context, $attribute);
280 if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) {
281 return $this->serializer->denormalize($data, $class, $format, $childContext);
285 // JSON only has a Number type corresponding to both int and float PHP types.
286 // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
287 // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
288 // PHP's json_decode automatically converts Numbers without a decimal part to integers.
289 // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
290 // a float is expected.
291 if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) {
292 return (float) $data;
295 if (\call_user_func('is_'.$builtinType, $data)) {
300 if (!empty($context[self::DISABLE_TYPE_ENFORCEMENT])) {
304 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)));
308 * Sets an attribute and apply the name converter if necessary.
310 * @param string $attribute
311 * @param mixed $attributeValue
315 private function updateData(array $data, $attribute, $attributeValue)
317 if ($this->nameConverter) {
318 $attribute = $this->nameConverter->normalize($attribute);
321 $data[$attribute] = $attributeValue;
327 * Is the max depth reached for the given attribute?
329 * @param AttributeMetadataInterface[] $attributesMetadata
330 * @param string $class
331 * @param string $attribute
332 * @param array $context
336 private function isMaxDepthReached(array $attributesMetadata, $class, $attribute, array &$context)
339 !isset($context[static::ENABLE_MAX_DEPTH]) ||
340 !$context[static::ENABLE_MAX_DEPTH] ||
341 !isset($attributesMetadata[$attribute]) ||
342 null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
347 $key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute);
348 if (!isset($context[$key])) {
354 if ($context[$key] === $maxDepth) {
364 * Gets the cache key to use.
366 * @param string|null $format
367 * @param array $context
369 * @return bool|string
371 private function getCacheKey($format, array $context)
374 return md5($format.serialize($context));
375 } catch (\Exception $exception) {
376 // The context cannot be serialized, skip the cache