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\Serializer\Exception\CircularReferenceException;
15 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
16 use Symfony\Component\Serializer\Exception\LogicException;
17 use Symfony\Component\Serializer\Exception\RuntimeException;
18 use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
19 use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
20 use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
21 use Symfony\Component\Serializer\SerializerAwareInterface;
24 * Normalizer implementation.
26 * @author Kévin Dunglas <dunglas@gmail.com>
28 abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
30 const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
31 const OBJECT_TO_POPULATE = 'object_to_populate';
32 const GROUPS = 'groups';
37 protected $circularReferenceLimit = 1;
42 protected $circularReferenceHandler;
45 * @var ClassMetadataFactoryInterface|null
47 protected $classMetadataFactory;
50 * @var NameConverterInterface|null
52 protected $nameConverter;
57 protected $callbacks = array();
62 protected $ignoredAttributes = array();
67 protected $camelizedAttributes = array();
70 * Sets the {@link ClassMetadataFactoryInterface} to use.
72 * @param ClassMetadataFactoryInterface|null $classMetadataFactory
73 * @param NameConverterInterface|null $nameConverter
75 public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null)
77 $this->classMetadataFactory = $classMetadataFactory;
78 $this->nameConverter = $nameConverter;
82 * Set circular reference limit.
84 * @param int $circularReferenceLimit limit of iterations for the same object
88 public function setCircularReferenceLimit($circularReferenceLimit)
90 $this->circularReferenceLimit = $circularReferenceLimit;
96 * Set circular reference handler.
98 * @param callable $circularReferenceHandler
102 public function setCircularReferenceHandler(callable $circularReferenceHandler)
104 $this->circularReferenceHandler = $circularReferenceHandler;
110 * Set normalization callbacks.
112 * @param callable[] $callbacks help normalize the result
116 * @throws InvalidArgumentException if a non-callable callback is set
118 public function setCallbacks(array $callbacks)
120 foreach ($callbacks as $attribute => $callback) {
121 if (!is_callable($callback)) {
122 throw new InvalidArgumentException(sprintf(
123 'The given callback for attribute "%s" is not callable.',
128 $this->callbacks = $callbacks;
134 * Set ignored attributes for normalization and denormalization.
136 * @param array $ignoredAttributes
140 public function setIgnoredAttributes(array $ignoredAttributes)
142 $this->ignoredAttributes = $ignoredAttributes;
148 * Detects if the configured circular reference limit is reached.
150 * @param object $object
151 * @param array $context
155 * @throws CircularReferenceException
157 protected function isCircularReference($object, &$context)
159 $objectHash = spl_object_hash($object);
161 if (isset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash])) {
162 if ($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] >= $this->circularReferenceLimit) {
163 unset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]);
168 ++$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash];
170 $context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] = 1;
177 * Handles a circular reference.
179 * If a circular reference handler is set, it will be called. Otherwise, a
180 * {@class CircularReferenceException} will be thrown.
182 * @param object $object
186 * @throws CircularReferenceException
188 protected function handleCircularReference($object)
190 if ($this->circularReferenceHandler) {
191 return call_user_func($this->circularReferenceHandler, $object);
194 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));
198 * Gets attributes to normalize using groups.
200 * @param string|object $classOrObject
201 * @param array $context
202 * @param bool $attributesAsString If false, return an array of {@link AttributeMetadataInterface}
204 * @return string[]|AttributeMetadataInterface[]|bool
206 protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
208 if (!$this->classMetadataFactory || !isset($context[static::GROUPS]) || !is_array($context[static::GROUPS])) {
212 $allowedAttributes = array();
213 foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) {
214 $name = $attributeMetadata->getName();
217 count(array_intersect($attributeMetadata->getGroups(), $context[static::GROUPS])) &&
218 $this->isAllowedAttribute($classOrObject, $name, null, $context)
220 $allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata;
224 return $allowedAttributes;
228 * Is this attribute allowed?
230 * @param object|string $classOrObject
231 * @param string $attribute
232 * @param string|null $format
233 * @param array $context
237 protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = array())
239 return !in_array($attribute, $this->ignoredAttributes);
243 * Normalizes the given data to an array. It's particularly useful during
244 * the denormalization process.
246 * @param object|array $data
250 protected function prepareForDenormalization($data)
252 return (array) $data;
256 * Returns the method to use to construct an object. This method must be either
257 * the object constructor or static.
260 * @param string $class
261 * @param array $context
262 * @param \ReflectionClass $reflectionClass
263 * @param array|bool $allowedAttributes
265 * @return \ReflectionMethod|null
267 protected function getConstructor(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes)
269 return $reflectionClass->getConstructor();
273 * Instantiates an object using constructor parameters when needed.
275 * This method also allows to denormalize data into an existing object if
276 * it is present in the context with the object_to_populate. This object
277 * is removed from the context before being returned to avoid side effects
278 * when recursively normalizing an object graph.
281 * @param string $class
282 * @param array $context
283 * @param \ReflectionClass $reflectionClass
284 * @param array|bool $allowedAttributes
285 * @param string|null $format
289 * @throws RuntimeException
291 protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes/*, $format = null*/)
293 if (func_num_args() >= 6) {
294 $format = func_get_arg(5);
296 if (__CLASS__ !== get_class($this)) {
297 $r = new \ReflectionMethod($this, __FUNCTION__);
298 if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
299 @trigger_error(sprintf('Method %s::%s() will have a 6th `$format = null` argument in version 4.0. Not defining it is deprecated since 3.2.', get_class($this), __FUNCTION__), E_USER_DEPRECATED);
307 isset($context[static::OBJECT_TO_POPULATE]) &&
308 is_object($context[static::OBJECT_TO_POPULATE]) &&
309 $context[static::OBJECT_TO_POPULATE] instanceof $class
311 $object = $context[static::OBJECT_TO_POPULATE];
312 unset($context[static::OBJECT_TO_POPULATE]);
317 $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
319 $constructorParameters = $constructor->getParameters();
322 foreach ($constructorParameters as $constructorParameter) {
323 $paramName = $constructorParameter->name;
324 $key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName;
326 $allowed = $allowedAttributes === false || in_array($paramName, $allowedAttributes);
327 $ignored = in_array($paramName, $this->ignoredAttributes);
328 if (method_exists($constructorParameter, 'isVariadic') && $constructorParameter->isVariadic()) {
329 if ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) {
330 if (!is_array($data[$paramName])) {
331 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));
334 $params = array_merge($params, $data[$paramName]);
336 } elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) {
337 $parameterData = $data[$key];
339 if (null !== $constructorParameter->getClass()) {
340 if (!$this->serializer instanceof DenormalizerInterface) {
341 throw new LogicException(sprintf('Cannot create an instance of %s from serialized data because the serializer inject in "%s" is not a denormalizer', $constructorParameter->getClass(), static::class));
343 $parameterClass = $constructorParameter->getClass()->getName();
344 $parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $context);
346 } catch (\ReflectionException $e) {
347 throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $key), 0, $e);
350 // Don't run set for a parameter passed to the constructor
351 $params[] = $parameterData;
353 } elseif ($constructorParameter->isDefaultValueAvailable()) {
354 $params[] = $constructorParameter->getDefaultValue();
356 throw new RuntimeException(
358 'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.',
360 $constructorParameter->name
366 if ($constructor->isConstructor()) {
367 return $reflectionClass->newInstanceArgs($params);
369 return $constructor->invokeArgs(null, $params);