annotate vendor/symfony/serializer/Normalizer/AbstractNormalizer.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /*
Chris@0 4 * This file is part of the Symfony package.
Chris@0 5 *
Chris@0 6 * (c) Fabien Potencier <fabien@symfony.com>
Chris@0 7 *
Chris@0 8 * For the full copyright and license information, please view the LICENSE
Chris@0 9 * file that was distributed with this source code.
Chris@0 10 */
Chris@0 11
Chris@0 12 namespace Symfony\Component\Serializer\Normalizer;
Chris@0 13
Chris@0 14 use Symfony\Component\Serializer\Exception\CircularReferenceException;
Chris@0 15 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
Chris@0 16 use Symfony\Component\Serializer\Exception\LogicException;
Chris@0 17 use Symfony\Component\Serializer\Exception\RuntimeException;
Chris@17 18 use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
Chris@0 19 use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
Chris@0 20 use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
Chris@0 21 use Symfony\Component\Serializer\SerializerAwareInterface;
Chris@0 22
Chris@0 23 /**
Chris@0 24 * Normalizer implementation.
Chris@0 25 *
Chris@0 26 * @author Kévin Dunglas <dunglas@gmail.com>
Chris@0 27 */
Chris@0 28 abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
Chris@0 29 {
Chris@14 30 use ObjectToPopulateTrait;
Chris@14 31
Chris@0 32 const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
Chris@0 33 const OBJECT_TO_POPULATE = 'object_to_populate';
Chris@0 34 const GROUPS = 'groups';
Chris@14 35 const ATTRIBUTES = 'attributes';
Chris@14 36 const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
Chris@0 37
Chris@0 38 /**
Chris@0 39 * @var int
Chris@0 40 */
Chris@0 41 protected $circularReferenceLimit = 1;
Chris@0 42
Chris@0 43 /**
Chris@0 44 * @var callable
Chris@0 45 */
Chris@0 46 protected $circularReferenceHandler;
Chris@0 47
Chris@0 48 /**
Chris@0 49 * @var ClassMetadataFactoryInterface|null
Chris@0 50 */
Chris@0 51 protected $classMetadataFactory;
Chris@0 52
Chris@0 53 /**
Chris@0 54 * @var NameConverterInterface|null
Chris@0 55 */
Chris@0 56 protected $nameConverter;
Chris@0 57
Chris@0 58 /**
Chris@0 59 * @var array
Chris@0 60 */
Chris@17 61 protected $callbacks = [];
Chris@0 62
Chris@0 63 /**
Chris@0 64 * @var array
Chris@0 65 */
Chris@17 66 protected $ignoredAttributes = [];
Chris@0 67
Chris@0 68 /**
Chris@0 69 * @var array
Chris@0 70 */
Chris@17 71 protected $camelizedAttributes = [];
Chris@0 72
Chris@0 73 /**
Chris@0 74 * Sets the {@link ClassMetadataFactoryInterface} to use.
Chris@0 75 */
Chris@0 76 public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null)
Chris@0 77 {
Chris@0 78 $this->classMetadataFactory = $classMetadataFactory;
Chris@0 79 $this->nameConverter = $nameConverter;
Chris@0 80 }
Chris@0 81
Chris@0 82 /**
Chris@0 83 * Set circular reference limit.
Chris@0 84 *
Chris@14 85 * @param int $circularReferenceLimit Limit of iterations for the same object
Chris@0 86 *
Chris@0 87 * @return self
Chris@0 88 */
Chris@0 89 public function setCircularReferenceLimit($circularReferenceLimit)
Chris@0 90 {
Chris@0 91 $this->circularReferenceLimit = $circularReferenceLimit;
Chris@0 92
Chris@0 93 return $this;
Chris@0 94 }
Chris@0 95
Chris@0 96 /**
Chris@0 97 * Set circular reference handler.
Chris@0 98 *
Chris@0 99 * @param callable $circularReferenceHandler
Chris@0 100 *
Chris@0 101 * @return self
Chris@0 102 */
Chris@0 103 public function setCircularReferenceHandler(callable $circularReferenceHandler)
Chris@0 104 {
Chris@0 105 $this->circularReferenceHandler = $circularReferenceHandler;
Chris@0 106
Chris@0 107 return $this;
Chris@0 108 }
Chris@0 109
Chris@0 110 /**
Chris@0 111 * Set normalization callbacks.
Chris@0 112 *
Chris@14 113 * @param callable[] $callbacks Help normalize the result
Chris@0 114 *
Chris@0 115 * @return self
Chris@0 116 *
Chris@0 117 * @throws InvalidArgumentException if a non-callable callback is set
Chris@0 118 */
Chris@0 119 public function setCallbacks(array $callbacks)
Chris@0 120 {
Chris@0 121 foreach ($callbacks as $attribute => $callback) {
Chris@14 122 if (!\is_callable($callback)) {
Chris@17 123 throw new InvalidArgumentException(sprintf('The given callback for attribute "%s" is not callable.', $attribute));
Chris@0 124 }
Chris@0 125 }
Chris@0 126 $this->callbacks = $callbacks;
Chris@0 127
Chris@0 128 return $this;
Chris@0 129 }
Chris@0 130
Chris@0 131 /**
Chris@0 132 * Set ignored attributes for normalization and denormalization.
Chris@0 133 *
Chris@0 134 * @return self
Chris@0 135 */
Chris@0 136 public function setIgnoredAttributes(array $ignoredAttributes)
Chris@0 137 {
Chris@0 138 $this->ignoredAttributes = $ignoredAttributes;
Chris@0 139
Chris@0 140 return $this;
Chris@0 141 }
Chris@0 142
Chris@0 143 /**
Chris@0 144 * Detects if the configured circular reference limit is reached.
Chris@0 145 *
Chris@0 146 * @param object $object
Chris@0 147 * @param array $context
Chris@0 148 *
Chris@0 149 * @return bool
Chris@0 150 *
Chris@0 151 * @throws CircularReferenceException
Chris@0 152 */
Chris@0 153 protected function isCircularReference($object, &$context)
Chris@0 154 {
Chris@0 155 $objectHash = spl_object_hash($object);
Chris@0 156
Chris@0 157 if (isset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash])) {
Chris@0 158 if ($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] >= $this->circularReferenceLimit) {
Chris@0 159 unset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]);
Chris@0 160
Chris@0 161 return true;
Chris@0 162 }
Chris@0 163
Chris@0 164 ++$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash];
Chris@0 165 } else {
Chris@0 166 $context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] = 1;
Chris@0 167 }
Chris@0 168
Chris@0 169 return false;
Chris@0 170 }
Chris@0 171
Chris@0 172 /**
Chris@0 173 * Handles a circular reference.
Chris@0 174 *
Chris@0 175 * If a circular reference handler is set, it will be called. Otherwise, a
Chris@0 176 * {@class CircularReferenceException} will be thrown.
Chris@0 177 *
Chris@0 178 * @param object $object
Chris@0 179 *
Chris@0 180 * @return mixed
Chris@0 181 *
Chris@0 182 * @throws CircularReferenceException
Chris@0 183 */
Chris@0 184 protected function handleCircularReference($object)
Chris@0 185 {
Chris@0 186 if ($this->circularReferenceHandler) {
Chris@14 187 return \call_user_func($this->circularReferenceHandler, $object);
Chris@0 188 }
Chris@0 189
Chris@14 190 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));
Chris@0 191 }
Chris@0 192
Chris@0 193 /**
Chris@0 194 * Gets attributes to normalize using groups.
Chris@0 195 *
Chris@0 196 * @param string|object $classOrObject
Chris@0 197 * @param array $context
Chris@0 198 * @param bool $attributesAsString If false, return an array of {@link AttributeMetadataInterface}
Chris@0 199 *
Chris@16 200 * @throws LogicException if the 'allow_extra_attributes' context variable is false and no class metadata factory is provided
Chris@16 201 *
Chris@0 202 * @return string[]|AttributeMetadataInterface[]|bool
Chris@0 203 */
Chris@0 204 protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
Chris@0 205 {
Chris@14 206 if (!$this->classMetadataFactory) {
Chris@16 207 if (isset($context[static::ALLOW_EXTRA_ATTRIBUTES]) && !$context[static::ALLOW_EXTRA_ATTRIBUTES]) {
Chris@16 208 throw new LogicException(sprintf('A class metadata factory must be provided in the constructor when setting "%s" to false.', static::ALLOW_EXTRA_ATTRIBUTES));
Chris@16 209 }
Chris@16 210
Chris@14 211 return false;
Chris@14 212 }
Chris@14 213
Chris@14 214 $groups = false;
Chris@14 215 if (isset($context[static::GROUPS]) && \is_array($context[static::GROUPS])) {
Chris@14 216 $groups = $context[static::GROUPS];
Chris@14 217 } elseif (!isset($context[static::ALLOW_EXTRA_ATTRIBUTES]) || $context[static::ALLOW_EXTRA_ATTRIBUTES]) {
Chris@0 218 return false;
Chris@0 219 }
Chris@0 220
Chris@17 221 $allowedAttributes = [];
Chris@0 222 foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) {
Chris@0 223 $name = $attributeMetadata->getName();
Chris@0 224
Chris@0 225 if (
Chris@14 226 (false === $groups || array_intersect($attributeMetadata->getGroups(), $groups)) &&
Chris@0 227 $this->isAllowedAttribute($classOrObject, $name, null, $context)
Chris@0 228 ) {
Chris@0 229 $allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata;
Chris@0 230 }
Chris@0 231 }
Chris@0 232
Chris@0 233 return $allowedAttributes;
Chris@0 234 }
Chris@0 235
Chris@0 236 /**
Chris@0 237 * Is this attribute allowed?
Chris@0 238 *
Chris@0 239 * @param object|string $classOrObject
Chris@0 240 * @param string $attribute
Chris@0 241 * @param string|null $format
Chris@0 242 * @param array $context
Chris@0 243 *
Chris@0 244 * @return bool
Chris@0 245 */
Chris@17 246 protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = [])
Chris@0 247 {
Chris@17 248 if (\in_array($attribute, $this->ignoredAttributes)) {
Chris@14 249 return false;
Chris@14 250 }
Chris@14 251
Chris@14 252 if (isset($context[self::ATTRIBUTES][$attribute])) {
Chris@14 253 // Nested attributes
Chris@14 254 return true;
Chris@14 255 }
Chris@14 256
Chris@17 257 if (isset($context[self::ATTRIBUTES]) && \is_array($context[self::ATTRIBUTES])) {
Chris@17 258 return \in_array($attribute, $context[self::ATTRIBUTES], true);
Chris@14 259 }
Chris@14 260
Chris@14 261 return true;
Chris@0 262 }
Chris@0 263
Chris@0 264 /**
Chris@0 265 * Normalizes the given data to an array. It's particularly useful during
Chris@0 266 * the denormalization process.
Chris@0 267 *
Chris@0 268 * @param object|array $data
Chris@0 269 *
Chris@0 270 * @return array
Chris@0 271 */
Chris@0 272 protected function prepareForDenormalization($data)
Chris@0 273 {
Chris@0 274 return (array) $data;
Chris@0 275 }
Chris@0 276
Chris@0 277 /**
Chris@0 278 * Returns the method to use to construct an object. This method must be either
Chris@0 279 * the object constructor or static.
Chris@0 280 *
Chris@0 281 * @param array $data
Chris@0 282 * @param string $class
Chris@0 283 * @param array $context
Chris@0 284 * @param \ReflectionClass $reflectionClass
Chris@0 285 * @param array|bool $allowedAttributes
Chris@0 286 *
Chris@0 287 * @return \ReflectionMethod|null
Chris@0 288 */
Chris@0 289 protected function getConstructor(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes)
Chris@0 290 {
Chris@0 291 return $reflectionClass->getConstructor();
Chris@0 292 }
Chris@0 293
Chris@0 294 /**
Chris@0 295 * Instantiates an object using constructor parameters when needed.
Chris@0 296 *
Chris@0 297 * This method also allows to denormalize data into an existing object if
Chris@0 298 * it is present in the context with the object_to_populate. This object
Chris@0 299 * is removed from the context before being returned to avoid side effects
Chris@0 300 * when recursively normalizing an object graph.
Chris@0 301 *
Chris@0 302 * @param array $data
Chris@0 303 * @param string $class
Chris@0 304 * @param array $context
Chris@0 305 * @param \ReflectionClass $reflectionClass
Chris@0 306 * @param array|bool $allowedAttributes
Chris@0 307 * @param string|null $format
Chris@0 308 *
Chris@0 309 * @return object
Chris@0 310 *
Chris@0 311 * @throws RuntimeException
Chris@0 312 */
Chris@14 313 protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes/*, string $format = null*/)
Chris@0 314 {
Chris@14 315 if (\func_num_args() >= 6) {
Chris@14 316 $format = \func_get_arg(5);
Chris@0 317 } else {
Chris@14 318 if (__CLASS__ !== \get_class($this)) {
Chris@0 319 $r = new \ReflectionMethod($this, __FUNCTION__);
Chris@0 320 if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
Chris@17 321 @trigger_error(sprintf('Method %s::%s() will have a 6th `string $format = null` argument in version 4.0. Not defining it is deprecated since Symfony 3.2.', \get_class($this), __FUNCTION__), E_USER_DEPRECATED);
Chris@0 322 }
Chris@0 323 }
Chris@0 324
Chris@0 325 $format = null;
Chris@0 326 }
Chris@0 327
Chris@14 328 if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
Chris@0 329 unset($context[static::OBJECT_TO_POPULATE]);
Chris@0 330
Chris@0 331 return $object;
Chris@0 332 }
Chris@18 333 // clean up even if no match
Chris@18 334 unset($context[static::OBJECT_TO_POPULATE]);
Chris@0 335
Chris@0 336 $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
Chris@0 337 if ($constructor) {
Chris@18 338 if (true !== $constructor->isPublic()) {
Chris@18 339 return $reflectionClass->newInstanceWithoutConstructor();
Chris@18 340 }
Chris@18 341
Chris@0 342 $constructorParameters = $constructor->getParameters();
Chris@0 343
Chris@17 344 $params = [];
Chris@0 345 foreach ($constructorParameters as $constructorParameter) {
Chris@0 346 $paramName = $constructorParameter->name;
Chris@0 347 $key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName;
Chris@0 348
Chris@14 349 $allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes);
Chris@14 350 $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
Chris@0 351 if (method_exists($constructorParameter, 'isVariadic') && $constructorParameter->isVariadic()) {
Chris@18 352 if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
Chris@14 353 if (!\is_array($data[$paramName])) {
Chris@0 354 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 355 }
Chris@0 356
Chris@0 357 $params = array_merge($params, $data[$paramName]);
Chris@0 358 }
Chris@18 359 } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
Chris@0 360 $parameterData = $data[$key];
Chris@16 361 if (null === $parameterData && $constructorParameter->allowsNull()) {
Chris@16 362 $params[] = null;
Chris@16 363 // Don't run set for a parameter passed to the constructor
Chris@16 364 unset($data[$key]);
Chris@16 365 continue;
Chris@16 366 }
Chris@0 367
Chris@0 368 // Don't run set for a parameter passed to the constructor
Chris@17 369 $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format);
Chris@0 370 unset($data[$key]);
Chris@0 371 } elseif ($constructorParameter->isDefaultValueAvailable()) {
Chris@0 372 $params[] = $constructorParameter->getDefaultValue();
Chris@0 373 } else {
Chris@17 374 throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
Chris@0 375 }
Chris@0 376 }
Chris@0 377
Chris@0 378 if ($constructor->isConstructor()) {
Chris@0 379 return $reflectionClass->newInstanceArgs($params);
Chris@0 380 } else {
Chris@0 381 return $constructor->invokeArgs(null, $params);
Chris@0 382 }
Chris@0 383 }
Chris@0 384
Chris@0 385 return new $class();
Chris@0 386 }
Chris@14 387
Chris@14 388 /**
Chris@17 389 * @internal
Chris@17 390 */
Chris@17 391 protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null)
Chris@17 392 {
Chris@17 393 try {
Chris@17 394 if (null !== $parameter->getClass()) {
Chris@17 395 if (!$this->serializer instanceof DenormalizerInterface) {
Chris@17 396 throw new LogicException(sprintf('Cannot create an instance of %s from serialized data because the serializer inject in "%s" is not a denormalizer', $parameter->getClass(), static::class));
Chris@17 397 }
Chris@17 398 $parameterClass = $parameter->getClass()->getName();
Chris@17 399
Chris@18 400 return $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format));
Chris@17 401 }
Chris@17 402
Chris@17 403 return $parameterData;
Chris@17 404 } catch (\ReflectionException $e) {
Chris@17 405 throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e);
Chris@17 406 }
Chris@17 407 }
Chris@17 408
Chris@17 409 /**
Chris@18 410 * @param array $parentContext
Chris@18 411 * @param string $attribute Attribute name
Chris@18 412 * @param string|null $format
Chris@14 413 *
Chris@14 414 * @return array
Chris@14 415 *
Chris@14 416 * @internal
Chris@14 417 */
Chris@18 418 protected function createChildContext(array $parentContext, $attribute/*, string $format = null */)
Chris@14 419 {
Chris@14 420 if (isset($parentContext[self::ATTRIBUTES][$attribute])) {
Chris@14 421 $parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute];
Chris@14 422 } else {
Chris@14 423 unset($parentContext[self::ATTRIBUTES]);
Chris@14 424 }
Chris@14 425
Chris@14 426 return $parentContext;
Chris@14 427 }
Chris@0 428 }