annotate vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.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\PropertyAccess\Exception\InvalidArgumentException;
Chris@17 15 use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
Chris@17 16 use Symfony\Component\PropertyInfo\Type;
Chris@0 17 use Symfony\Component\Serializer\Encoder\JsonEncoder;
Chris@14 18 use Symfony\Component\Serializer\Exception\ExtraAttributesException;
Chris@0 19 use Symfony\Component\Serializer\Exception\LogicException;
Chris@14 20 use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
Chris@0 21 use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
Chris@0 22 use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
Chris@0 23 use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
Chris@0 24
Chris@0 25 /**
Chris@0 26 * Base class for a normalizer dealing with objects.
Chris@0 27 *
Chris@0 28 * @author Kévin Dunglas <dunglas@gmail.com>
Chris@0 29 */
Chris@0 30 abstract class AbstractObjectNormalizer extends AbstractNormalizer
Chris@0 31 {
Chris@0 32 const ENABLE_MAX_DEPTH = 'enable_max_depth';
Chris@0 33 const DEPTH_KEY_PATTERN = 'depth_%s::%s';
Chris@14 34 const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
Chris@0 35
Chris@0 36 private $propertyTypeExtractor;
Chris@17 37 private $attributesCache = [];
Chris@17 38 private $cache = [];
Chris@0 39
Chris@0 40 public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
Chris@0 41 {
Chris@0 42 parent::__construct($classMetadataFactory, $nameConverter);
Chris@0 43
Chris@0 44 $this->propertyTypeExtractor = $propertyTypeExtractor;
Chris@0 45 }
Chris@0 46
Chris@0 47 /**
Chris@0 48 * {@inheritdoc}
Chris@0 49 */
Chris@0 50 public function supportsNormalization($data, $format = null)
Chris@0 51 {
Chris@14 52 return \is_object($data) && !$data instanceof \Traversable;
Chris@0 53 }
Chris@0 54
Chris@0 55 /**
Chris@0 56 * {@inheritdoc}
Chris@0 57 */
Chris@17 58 public function normalize($object, $format = null, array $context = [])
Chris@0 59 {
Chris@0 60 if (!isset($context['cache_key'])) {
Chris@0 61 $context['cache_key'] = $this->getCacheKey($format, $context);
Chris@0 62 }
Chris@0 63
Chris@0 64 if ($this->isCircularReference($object, $context)) {
Chris@0 65 return $this->handleCircularReference($object);
Chris@0 66 }
Chris@0 67
Chris@17 68 $data = [];
Chris@17 69 $stack = [];
Chris@0 70 $attributes = $this->getAttributes($object, $format, $context);
Chris@17 71 $class = \get_class($object);
Chris@0 72 $attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
Chris@0 73
Chris@0 74 foreach ($attributes as $attribute) {
Chris@0 75 if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) {
Chris@0 76 continue;
Chris@0 77 }
Chris@0 78
Chris@0 79 $attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
Chris@0 80
Chris@0 81 if (isset($this->callbacks[$attribute])) {
Chris@17 82 $attributeValue = \call_user_func($this->callbacks[$attribute], $attributeValue);
Chris@0 83 }
Chris@0 84
Chris@0 85 if (null !== $attributeValue && !is_scalar($attributeValue)) {
Chris@0 86 $stack[$attribute] = $attributeValue;
Chris@0 87 }
Chris@0 88
Chris@0 89 $data = $this->updateData($data, $attribute, $attributeValue);
Chris@0 90 }
Chris@0 91
Chris@0 92 foreach ($stack as $attribute => $attributeValue) {
Chris@0 93 if (!$this->serializer instanceof NormalizerInterface) {
Chris@0 94 throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer', $attribute));
Chris@0 95 }
Chris@0 96
Chris@18 97 $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)));
Chris@0 98 }
Chris@0 99
Chris@0 100 return $data;
Chris@0 101 }
Chris@0 102
Chris@0 103 /**
Chris@0 104 * Gets and caches attributes for the given object, format and context.
Chris@0 105 *
Chris@0 106 * @param object $object
Chris@0 107 * @param string|null $format
Chris@0 108 * @param array $context
Chris@0 109 *
Chris@0 110 * @return string[]
Chris@0 111 */
Chris@0 112 protected function getAttributes($object, $format = null, array $context)
Chris@0 113 {
Chris@17 114 $class = \get_class($object);
Chris@0 115 $key = $class.'-'.$context['cache_key'];
Chris@0 116
Chris@0 117 if (isset($this->attributesCache[$key])) {
Chris@0 118 return $this->attributesCache[$key];
Chris@0 119 }
Chris@0 120
Chris@0 121 $allowedAttributes = $this->getAllowedAttributes($object, $context, true);
Chris@0 122
Chris@0 123 if (false !== $allowedAttributes) {
Chris@0 124 if ($context['cache_key']) {
Chris@0 125 $this->attributesCache[$key] = $allowedAttributes;
Chris@0 126 }
Chris@0 127
Chris@0 128 return $allowedAttributes;
Chris@0 129 }
Chris@0 130
Chris@18 131 $attributes = $this->extractAttributes($object, $format, $context);
Chris@18 132
Chris@18 133 if ($context['cache_key']) {
Chris@18 134 $this->attributesCache[$key] = $attributes;
Chris@14 135 }
Chris@14 136
Chris@18 137 return $attributes;
Chris@0 138 }
Chris@0 139
Chris@0 140 /**
Chris@0 141 * Extracts attributes to normalize from the class of the given object, format and context.
Chris@0 142 *
Chris@0 143 * @param object $object
Chris@0 144 * @param string|null $format
Chris@0 145 * @param array $context
Chris@0 146 *
Chris@0 147 * @return string[]
Chris@0 148 */
Chris@17 149 abstract protected function extractAttributes($object, $format = null, array $context = []);
Chris@0 150
Chris@0 151 /**
Chris@0 152 * Gets the attribute value.
Chris@0 153 *
Chris@0 154 * @param object $object
Chris@0 155 * @param string $attribute
Chris@0 156 * @param string|null $format
Chris@0 157 * @param array $context
Chris@0 158 *
Chris@0 159 * @return mixed
Chris@0 160 */
Chris@17 161 abstract protected function getAttributeValue($object, $attribute, $format = null, array $context = []);
Chris@0 162
Chris@0 163 /**
Chris@0 164 * {@inheritdoc}
Chris@0 165 */
Chris@0 166 public function supportsDenormalization($data, $type, $format = null)
Chris@0 167 {
Chris@14 168 return isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = class_exists($type);
Chris@0 169 }
Chris@0 170
Chris@0 171 /**
Chris@0 172 * {@inheritdoc}
Chris@0 173 */
Chris@17 174 public function denormalize($data, $class, $format = null, array $context = [])
Chris@0 175 {
Chris@0 176 if (!isset($context['cache_key'])) {
Chris@0 177 $context['cache_key'] = $this->getCacheKey($format, $context);
Chris@0 178 }
Chris@14 179
Chris@0 180 $allowedAttributes = $this->getAllowedAttributes($class, $context, true);
Chris@0 181 $normalizedData = $this->prepareForDenormalization($data);
Chris@17 182 $extraAttributes = [];
Chris@0 183
Chris@0 184 $reflectionClass = new \ReflectionClass($class);
Chris@0 185 $object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format);
Chris@0 186
Chris@0 187 foreach ($normalizedData as $attribute => $value) {
Chris@0 188 if ($this->nameConverter) {
Chris@0 189 $attribute = $this->nameConverter->denormalize($attribute);
Chris@0 190 }
Chris@0 191
Chris@17 192 if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($class, $attribute, $format, $context)) {
Chris@14 193 if (isset($context[self::ALLOW_EXTRA_ATTRIBUTES]) && !$context[self::ALLOW_EXTRA_ATTRIBUTES]) {
Chris@14 194 $extraAttributes[] = $attribute;
Chris@14 195 }
Chris@14 196
Chris@0 197 continue;
Chris@0 198 }
Chris@0 199
Chris@0 200 $value = $this->validateAndDenormalize($class, $attribute, $value, $format, $context);
Chris@0 201 try {
Chris@0 202 $this->setAttributeValue($object, $attribute, $value, $format, $context);
Chris@0 203 } catch (InvalidArgumentException $e) {
Chris@14 204 throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
Chris@0 205 }
Chris@0 206 }
Chris@0 207
Chris@14 208 if (!empty($extraAttributes)) {
Chris@14 209 throw new ExtraAttributesException($extraAttributes);
Chris@14 210 }
Chris@14 211
Chris@0 212 return $object;
Chris@0 213 }
Chris@0 214
Chris@0 215 /**
Chris@0 216 * Sets attribute value.
Chris@0 217 *
Chris@0 218 * @param object $object
Chris@0 219 * @param string $attribute
Chris@0 220 * @param mixed $value
Chris@0 221 * @param string|null $format
Chris@0 222 * @param array $context
Chris@0 223 */
Chris@17 224 abstract protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []);
Chris@0 225
Chris@0 226 /**
Chris@0 227 * Validates the submitted data and denormalizes it.
Chris@0 228 *
Chris@0 229 * @param string $currentClass
Chris@0 230 * @param string $attribute
Chris@0 231 * @param mixed $data
Chris@0 232 * @param string|null $format
Chris@0 233 * @param array $context
Chris@0 234 *
Chris@0 235 * @return mixed
Chris@0 236 *
Chris@14 237 * @throws NotNormalizableValueException
Chris@0 238 * @throws LogicException
Chris@0 239 */
Chris@0 240 private function validateAndDenormalize($currentClass, $attribute, $data, $format, array $context)
Chris@0 241 {
Chris@0 242 if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
Chris@0 243 return $data;
Chris@0 244 }
Chris@0 245
Chris@17 246 $expectedTypes = [];
Chris@0 247 foreach ($types as $type) {
Chris@0 248 if (null === $data && $type->isNullable()) {
Chris@0 249 return;
Chris@0 250 }
Chris@0 251
Chris@0 252 if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
Chris@0 253 $builtinType = Type::BUILTIN_TYPE_OBJECT;
Chris@0 254 $class = $collectionValueType->getClassName().'[]';
Chris@0 255
Chris@16 256 // Fix a collection that contains the only one element
Chris@16 257 // This is special to xml format only
Chris@17 258 if ('xml' === $format && !\is_int(key($data))) {
Chris@17 259 $data = [$data];
Chris@16 260 }
Chris@16 261
Chris@0 262 if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
Chris@0 263 $context['key_type'] = $collectionKeyType;
Chris@0 264 }
Chris@0 265 } else {
Chris@0 266 $builtinType = $type->getBuiltinType();
Chris@0 267 $class = $type->getClassName();
Chris@0 268 }
Chris@0 269
Chris@0 270 $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
Chris@0 271
Chris@0 272 if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
Chris@0 273 if (!$this->serializer instanceof DenormalizerInterface) {
Chris@0 274 throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer', $attribute, $class));
Chris@0 275 }
Chris@0 276
Chris@18 277 $childContext = $this->createChildContext($context, $attribute, $format);
Chris@14 278 if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) {
Chris@14 279 return $this->serializer->denormalize($data, $class, $format, $childContext);
Chris@0 280 }
Chris@0 281 }
Chris@0 282
Chris@0 283 // JSON only has a Number type corresponding to both int and float PHP types.
Chris@0 284 // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
Chris@0 285 // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
Chris@0 286 // PHP's json_decode automatically converts Numbers without a decimal part to integers.
Chris@0 287 // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
Chris@0 288 // a float is expected.
Chris@17 289 if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) {
Chris@0 290 return (float) $data;
Chris@0 291 }
Chris@0 292
Chris@17 293 if (\call_user_func('is_'.$builtinType, $data)) {
Chris@0 294 return $data;
Chris@0 295 }
Chris@0 296 }
Chris@0 297
Chris@14 298 if (!empty($context[self::DISABLE_TYPE_ENFORCEMENT])) {
Chris@14 299 return $data;
Chris@14 300 }
Chris@14 301
Chris@17 302 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)));
Chris@17 303 }
Chris@17 304
Chris@17 305 /**
Chris@17 306 * @internal
Chris@17 307 */
Chris@17 308 protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null)
Chris@17 309 {
Chris@17 310 if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) {
Chris@17 311 return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format);
Chris@17 312 }
Chris@17 313
Chris@17 314 return $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context);
Chris@0 315 }
Chris@0 316
Chris@0 317 /**
Chris@0 318 * Sets an attribute and apply the name converter if necessary.
Chris@0 319 *
Chris@0 320 * @param string $attribute
Chris@0 321 * @param mixed $attributeValue
Chris@0 322 *
Chris@0 323 * @return array
Chris@0 324 */
Chris@0 325 private function updateData(array $data, $attribute, $attributeValue)
Chris@0 326 {
Chris@0 327 if ($this->nameConverter) {
Chris@0 328 $attribute = $this->nameConverter->normalize($attribute);
Chris@0 329 }
Chris@0 330
Chris@0 331 $data[$attribute] = $attributeValue;
Chris@0 332
Chris@0 333 return $data;
Chris@0 334 }
Chris@0 335
Chris@0 336 /**
Chris@0 337 * Is the max depth reached for the given attribute?
Chris@0 338 *
Chris@0 339 * @param AttributeMetadataInterface[] $attributesMetadata
Chris@0 340 * @param string $class
Chris@0 341 * @param string $attribute
Chris@0 342 * @param array $context
Chris@0 343 *
Chris@0 344 * @return bool
Chris@0 345 */
Chris@0 346 private function isMaxDepthReached(array $attributesMetadata, $class, $attribute, array &$context)
Chris@0 347 {
Chris@0 348 if (
Chris@0 349 !isset($context[static::ENABLE_MAX_DEPTH]) ||
Chris@16 350 !$context[static::ENABLE_MAX_DEPTH] ||
Chris@0 351 !isset($attributesMetadata[$attribute]) ||
Chris@0 352 null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
Chris@0 353 ) {
Chris@0 354 return false;
Chris@0 355 }
Chris@0 356
Chris@0 357 $key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute);
Chris@0 358 if (!isset($context[$key])) {
Chris@0 359 $context[$key] = 1;
Chris@0 360
Chris@0 361 return false;
Chris@0 362 }
Chris@0 363
Chris@0 364 if ($context[$key] === $maxDepth) {
Chris@0 365 return true;
Chris@0 366 }
Chris@0 367
Chris@0 368 ++$context[$key];
Chris@0 369
Chris@0 370 return false;
Chris@0 371 }
Chris@0 372
Chris@0 373 /**
Chris@18 374 * Overwritten to update the cache key for the child.
Chris@18 375 *
Chris@18 376 * We must not mix up the attribute cache between parent and children.
Chris@18 377 *
Chris@18 378 * {@inheritdoc}
Chris@18 379 */
Chris@18 380 protected function createChildContext(array $parentContext, $attribute/*, string $format = null */)
Chris@18 381 {
Chris@18 382 if (\func_num_args() >= 3) {
Chris@18 383 $format = \func_get_arg(2);
Chris@18 384 } else {
Chris@18 385 // will be deprecated in version 4
Chris@18 386 $format = null;
Chris@18 387 }
Chris@18 388
Chris@18 389 $context = parent::createChildContext($parentContext, $attribute, $format);
Chris@18 390 // format is already included in the cache_key of the parent.
Chris@18 391 $context['cache_key'] = $this->getCacheKey($format, $context);
Chris@18 392
Chris@18 393 return $context;
Chris@18 394 }
Chris@18 395
Chris@18 396 /**
Chris@18 397 * Builds the cache key for the attributes cache.
Chris@18 398 *
Chris@18 399 * The key must be different for every option in the context that could change which attributes should be handled.
Chris@0 400 *
Chris@0 401 * @param string|null $format
Chris@0 402 * @param array $context
Chris@0 403 *
Chris@0 404 * @return bool|string
Chris@0 405 */
Chris@0 406 private function getCacheKey($format, array $context)
Chris@0 407 {
Chris@18 408 unset($context['cache_key']); // avoid artificially different keys
Chris@0 409 try {
Chris@18 410 return md5($format.serialize([
Chris@18 411 'context' => $context,
Chris@18 412 'ignored' => $this->ignoredAttributes,
Chris@18 413 'camelized' => $this->camelizedAttributes,
Chris@18 414 ]));
Chris@0 415 } catch (\Exception $exception) {
Chris@0 416 // The context cannot be serialized, skip the cache
Chris@0 417 return false;
Chris@0 418 }
Chris@0 419 }
Chris@0 420 }