Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Serializer\Normalizer; Chris@0: Chris@0: use Symfony\Component\Serializer\Exception\CircularReferenceException; Chris@0: use Symfony\Component\Serializer\Exception\InvalidArgumentException; Chris@0: use Symfony\Component\Serializer\Exception\LogicException; Chris@0: use Symfony\Component\Serializer\Exception\RuntimeException; Chris@0: use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; Chris@0: use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; Chris@0: use Symfony\Component\Serializer\NameConverter\NameConverterInterface; Chris@0: use Symfony\Component\Serializer\SerializerAwareInterface; Chris@0: Chris@0: /** Chris@0: * Normalizer implementation. Chris@0: * Chris@0: * @author Kévin Dunglas Chris@0: */ Chris@0: abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface Chris@0: { Chris@0: const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit'; Chris@0: const OBJECT_TO_POPULATE = 'object_to_populate'; Chris@0: const GROUPS = 'groups'; Chris@0: Chris@0: /** Chris@0: * @var int Chris@0: */ Chris@0: protected $circularReferenceLimit = 1; Chris@0: Chris@0: /** Chris@0: * @var callable Chris@0: */ Chris@0: protected $circularReferenceHandler; Chris@0: Chris@0: /** Chris@0: * @var ClassMetadataFactoryInterface|null Chris@0: */ Chris@0: protected $classMetadataFactory; Chris@0: Chris@0: /** Chris@0: * @var NameConverterInterface|null Chris@0: */ Chris@0: protected $nameConverter; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@0: protected $callbacks = array(); Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@0: protected $ignoredAttributes = array(); Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@0: protected $camelizedAttributes = array(); Chris@0: Chris@0: /** Chris@0: * Sets the {@link ClassMetadataFactoryInterface} to use. Chris@0: * Chris@0: * @param ClassMetadataFactoryInterface|null $classMetadataFactory Chris@0: * @param NameConverterInterface|null $nameConverter Chris@0: */ Chris@0: public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null) Chris@0: { Chris@0: $this->classMetadataFactory = $classMetadataFactory; Chris@0: $this->nameConverter = $nameConverter; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set circular reference limit. Chris@0: * Chris@0: * @param int $circularReferenceLimit limit of iterations for the same object Chris@0: * Chris@0: * @return self Chris@0: */ Chris@0: public function setCircularReferenceLimit($circularReferenceLimit) Chris@0: { Chris@0: $this->circularReferenceLimit = $circularReferenceLimit; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set circular reference handler. Chris@0: * Chris@0: * @param callable $circularReferenceHandler Chris@0: * Chris@0: * @return self Chris@0: */ Chris@0: public function setCircularReferenceHandler(callable $circularReferenceHandler) Chris@0: { Chris@0: $this->circularReferenceHandler = $circularReferenceHandler; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set normalization callbacks. Chris@0: * Chris@0: * @param callable[] $callbacks help normalize the result Chris@0: * Chris@0: * @return self Chris@0: * Chris@0: * @throws InvalidArgumentException if a non-callable callback is set Chris@0: */ Chris@0: public function setCallbacks(array $callbacks) Chris@0: { Chris@0: foreach ($callbacks as $attribute => $callback) { Chris@0: if (!is_callable($callback)) { Chris@0: throw new InvalidArgumentException(sprintf( Chris@0: 'The given callback for attribute "%s" is not callable.', Chris@0: $attribute Chris@0: )); Chris@0: } Chris@0: } Chris@0: $this->callbacks = $callbacks; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set ignored attributes for normalization and denormalization. Chris@0: * Chris@0: * @param array $ignoredAttributes Chris@0: * Chris@0: * @return self Chris@0: */ Chris@0: public function setIgnoredAttributes(array $ignoredAttributes) Chris@0: { Chris@0: $this->ignoredAttributes = $ignoredAttributes; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Detects if the configured circular reference limit is reached. Chris@0: * Chris@0: * @param object $object Chris@0: * @param array $context Chris@0: * Chris@0: * @return bool Chris@0: * Chris@0: * @throws CircularReferenceException Chris@0: */ Chris@0: protected function isCircularReference($object, &$context) Chris@0: { Chris@0: $objectHash = spl_object_hash($object); Chris@0: Chris@0: if (isset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash])) { Chris@0: if ($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] >= $this->circularReferenceLimit) { Chris@0: unset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]); Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: ++$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]; Chris@0: } else { Chris@0: $context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] = 1; Chris@0: } Chris@0: Chris@0: return false; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Handles a circular reference. Chris@0: * Chris@0: * If a circular reference handler is set, it will be called. Otherwise, a Chris@0: * {@class CircularReferenceException} will be thrown. Chris@0: * Chris@0: * @param object $object Chris@0: * Chris@0: * @return mixed Chris@0: * Chris@0: * @throws CircularReferenceException Chris@0: */ Chris@0: protected function handleCircularReference($object) Chris@0: { Chris@0: if ($this->circularReferenceHandler) { Chris@0: return call_user_func($this->circularReferenceHandler, $object); Chris@0: } Chris@0: Chris@0: throw new CircularReferenceException(sprintf('A circular reference has been detected (configured limit: %d).', $this->circularReferenceLimit)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets attributes to normalize using groups. Chris@0: * Chris@0: * @param string|object $classOrObject Chris@0: * @param array $context Chris@0: * @param bool $attributesAsString If false, return an array of {@link AttributeMetadataInterface} Chris@0: * Chris@0: * @return string[]|AttributeMetadataInterface[]|bool Chris@0: */ Chris@0: protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false) Chris@0: { Chris@0: if (!$this->classMetadataFactory || !isset($context[static::GROUPS]) || !is_array($context[static::GROUPS])) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $allowedAttributes = array(); Chris@0: foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) { Chris@0: $name = $attributeMetadata->getName(); Chris@0: Chris@0: if ( Chris@0: count(array_intersect($attributeMetadata->getGroups(), $context[static::GROUPS])) && Chris@0: $this->isAllowedAttribute($classOrObject, $name, null, $context) Chris@0: ) { Chris@0: $allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata; Chris@0: } Chris@0: } Chris@0: Chris@0: return $allowedAttributes; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Is this attribute allowed? Chris@0: * Chris@0: * @param object|string $classOrObject Chris@0: * @param string $attribute Chris@0: * @param string|null $format Chris@0: * @param array $context Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = array()) Chris@0: { Chris@0: return !in_array($attribute, $this->ignoredAttributes); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Normalizes the given data to an array. It's particularly useful during Chris@0: * the denormalization process. Chris@0: * Chris@0: * @param object|array $data Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: protected function prepareForDenormalization($data) Chris@0: { Chris@0: return (array) $data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the method to use to construct an object. This method must be either Chris@0: * the object constructor or static. Chris@0: * Chris@0: * @param array $data Chris@0: * @param string $class Chris@0: * @param array $context Chris@0: * @param \ReflectionClass $reflectionClass Chris@0: * @param array|bool $allowedAttributes Chris@0: * Chris@0: * @return \ReflectionMethod|null Chris@0: */ Chris@0: protected function getConstructor(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes) Chris@0: { Chris@0: return $reflectionClass->getConstructor(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Instantiates an object using constructor parameters when needed. Chris@0: * Chris@0: * This method also allows to denormalize data into an existing object if Chris@0: * it is present in the context with the object_to_populate. This object Chris@0: * is removed from the context before being returned to avoid side effects Chris@0: * when recursively normalizing an object graph. Chris@0: * Chris@0: * @param array $data Chris@0: * @param string $class Chris@0: * @param array $context Chris@0: * @param \ReflectionClass $reflectionClass Chris@0: * @param array|bool $allowedAttributes Chris@0: * @param string|null $format Chris@0: * Chris@0: * @return object Chris@0: * Chris@0: * @throws RuntimeException Chris@0: */ Chris@0: protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes/*, $format = null*/) Chris@0: { Chris@0: if (func_num_args() >= 6) { Chris@0: $format = func_get_arg(5); Chris@0: } else { Chris@0: if (__CLASS__ !== get_class($this)) { Chris@0: $r = new \ReflectionMethod($this, __FUNCTION__); Chris@0: if (__CLASS__ !== $r->getDeclaringClass()->getName()) { Chris@0: @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); Chris@0: } Chris@0: } Chris@0: Chris@0: $format = null; Chris@0: } Chris@0: Chris@0: if ( Chris@0: isset($context[static::OBJECT_TO_POPULATE]) && Chris@0: is_object($context[static::OBJECT_TO_POPULATE]) && Chris@0: $context[static::OBJECT_TO_POPULATE] instanceof $class Chris@0: ) { Chris@0: $object = $context[static::OBJECT_TO_POPULATE]; Chris@0: unset($context[static::OBJECT_TO_POPULATE]); Chris@0: Chris@0: return $object; Chris@0: } Chris@0: Chris@0: $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes); Chris@0: if ($constructor) { Chris@0: $constructorParameters = $constructor->getParameters(); Chris@0: Chris@0: $params = array(); Chris@0: foreach ($constructorParameters as $constructorParameter) { Chris@0: $paramName = $constructorParameter->name; Chris@0: $key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName; Chris@0: Chris@0: $allowed = $allowedAttributes === false || in_array($paramName, $allowedAttributes); Chris@0: $ignored = in_array($paramName, $this->ignoredAttributes); Chris@0: if (method_exists($constructorParameter, 'isVariadic') && $constructorParameter->isVariadic()) { Chris@0: if ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) { Chris@0: if (!is_array($data[$paramName])) { Chris@0: 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: } Chris@0: Chris@0: $params = array_merge($params, $data[$paramName]); Chris@0: } Chris@0: } elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) { Chris@0: $parameterData = $data[$key]; Chris@0: try { Chris@0: if (null !== $constructorParameter->getClass()) { Chris@0: if (!$this->serializer instanceof DenormalizerInterface) { Chris@0: 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)); Chris@0: } Chris@0: $parameterClass = $constructorParameter->getClass()->getName(); Chris@0: $parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $context); Chris@0: } Chris@0: } catch (\ReflectionException $e) { Chris@0: throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $key), 0, $e); Chris@0: } Chris@0: Chris@0: // Don't run set for a parameter passed to the constructor Chris@0: $params[] = $parameterData; Chris@0: unset($data[$key]); Chris@0: } elseif ($constructorParameter->isDefaultValueAvailable()) { Chris@0: $params[] = $constructorParameter->getDefaultValue(); Chris@0: } else { Chris@0: throw new RuntimeException( Chris@0: sprintf( Chris@0: 'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', Chris@0: $class, Chris@0: $constructorParameter->name Chris@0: ) Chris@0: ); Chris@0: } Chris@0: } Chris@0: Chris@0: if ($constructor->isConstructor()) { Chris@0: return $reflectionClass->newInstanceArgs($params); Chris@0: } else { Chris@0: return $constructor->invokeArgs(null, $params); Chris@0: } Chris@0: } Chris@0: Chris@0: return new $class(); Chris@0: } Chris@0: }