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\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;
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 if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
259 $context['key_type'] = $collectionKeyType;
262 $builtinType = $type->getBuiltinType();
263 $class = $type->getClassName();
266 $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
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));
273 $childContext = $this->createChildContext($context, $attribute);
274 if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) {
275 return $this->serializer->denormalize($data, $class, $format, $childContext);
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;
289 if (call_user_func('is_'.$builtinType, $data)) {
294 if (!empty($context[self::DISABLE_TYPE_ENFORCEMENT])) {
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)));
302 * Sets an attribute and apply the name converter if necessary.
305 * @param string $attribute
306 * @param mixed $attributeValue
310 private function updateData(array $data, $attribute, $attributeValue)
312 if ($this->nameConverter) {
313 $attribute = $this->nameConverter->normalize($attribute);
316 $data[$attribute] = $attributeValue;
322 * Is the max depth reached for the given attribute?
324 * @param AttributeMetadataInterface[] $attributesMetadata
325 * @param string $class
326 * @param string $attribute
327 * @param array $context
331 private function isMaxDepthReached(array $attributesMetadata, $class, $attribute, array &$context)
334 !isset($context[static::ENABLE_MAX_DEPTH]) ||
335 !isset($attributesMetadata[$attribute]) ||
336 null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
341 $key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute);
342 if (!isset($context[$key])) {
348 if ($context[$key] === $maxDepth) {
358 * Gets the cache key to use.
360 * @param string|null $format
361 * @param array $context
363 * @return bool|string
365 private function getCacheKey($format, array $context)
368 return md5($format.serialize($context));
369 } catch (\Exception $exception) {
370 // The context cannot be serialized, skip the cache