annotate vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php @ 17:129ea1e6d783

Update, including to Drupal core 8.6.10
author Chris Cannam
date Thu, 28 Feb 2019 13:21:36 +0000
parents c2387f117808
children af1871eacc83
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@14 97 $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute)));
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@14 131 if (isset($context['attributes'])) {
Chris@14 132 return $this->extractAttributes($object, $format, $context);
Chris@14 133 }
Chris@14 134
Chris@0 135 if (isset($this->attributesCache[$class])) {
Chris@0 136 return $this->attributesCache[$class];
Chris@0 137 }
Chris@0 138
Chris@0 139 return $this->attributesCache[$class] = $this->extractAttributes($object, $format, $context);
Chris@0 140 }
Chris@0 141
Chris@0 142 /**
Chris@0 143 * Extracts attributes to normalize from the class of the given object, format and context.
Chris@0 144 *
Chris@0 145 * @param object $object
Chris@0 146 * @param string|null $format
Chris@0 147 * @param array $context
Chris@0 148 *
Chris@0 149 * @return string[]
Chris@0 150 */
Chris@17 151 abstract protected function extractAttributes($object, $format = null, array $context = []);
Chris@0 152
Chris@0 153 /**
Chris@0 154 * Gets the attribute value.
Chris@0 155 *
Chris@0 156 * @param object $object
Chris@0 157 * @param string $attribute
Chris@0 158 * @param string|null $format
Chris@0 159 * @param array $context
Chris@0 160 *
Chris@0 161 * @return mixed
Chris@0 162 */
Chris@17 163 abstract protected function getAttributeValue($object, $attribute, $format = null, array $context = []);
Chris@0 164
Chris@0 165 /**
Chris@0 166 * {@inheritdoc}
Chris@0 167 */
Chris@0 168 public function supportsDenormalization($data, $type, $format = null)
Chris@0 169 {
Chris@14 170 return isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = class_exists($type);
Chris@0 171 }
Chris@0 172
Chris@0 173 /**
Chris@0 174 * {@inheritdoc}
Chris@0 175 */
Chris@17 176 public function denormalize($data, $class, $format = null, array $context = [])
Chris@0 177 {
Chris@0 178 if (!isset($context['cache_key'])) {
Chris@0 179 $context['cache_key'] = $this->getCacheKey($format, $context);
Chris@0 180 }
Chris@14 181
Chris@0 182 $allowedAttributes = $this->getAllowedAttributes($class, $context, true);
Chris@0 183 $normalizedData = $this->prepareForDenormalization($data);
Chris@17 184 $extraAttributes = [];
Chris@0 185
Chris@0 186 $reflectionClass = new \ReflectionClass($class);
Chris@0 187 $object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format);
Chris@0 188
Chris@0 189 foreach ($normalizedData as $attribute => $value) {
Chris@0 190 if ($this->nameConverter) {
Chris@0 191 $attribute = $this->nameConverter->denormalize($attribute);
Chris@0 192 }
Chris@0 193
Chris@17 194 if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($class, $attribute, $format, $context)) {
Chris@14 195 if (isset($context[self::ALLOW_EXTRA_ATTRIBUTES]) && !$context[self::ALLOW_EXTRA_ATTRIBUTES]) {
Chris@14 196 $extraAttributes[] = $attribute;
Chris@14 197 }
Chris@14 198
Chris@0 199 continue;
Chris@0 200 }
Chris@0 201
Chris@0 202 $value = $this->validateAndDenormalize($class, $attribute, $value, $format, $context);
Chris@0 203 try {
Chris@0 204 $this->setAttributeValue($object, $attribute, $value, $format, $context);
Chris@0 205 } catch (InvalidArgumentException $e) {
Chris@14 206 throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
Chris@0 207 }
Chris@0 208 }
Chris@0 209
Chris@14 210 if (!empty($extraAttributes)) {
Chris@14 211 throw new ExtraAttributesException($extraAttributes);
Chris@14 212 }
Chris@14 213
Chris@0 214 return $object;
Chris@0 215 }
Chris@0 216
Chris@0 217 /**
Chris@0 218 * Sets attribute value.
Chris@0 219 *
Chris@0 220 * @param object $object
Chris@0 221 * @param string $attribute
Chris@0 222 * @param mixed $value
Chris@0 223 * @param string|null $format
Chris@0 224 * @param array $context
Chris@0 225 */
Chris@17 226 abstract protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []);
Chris@0 227
Chris@0 228 /**
Chris@0 229 * Validates the submitted data and denormalizes it.
Chris@0 230 *
Chris@0 231 * @param string $currentClass
Chris@0 232 * @param string $attribute
Chris@0 233 * @param mixed $data
Chris@0 234 * @param string|null $format
Chris@0 235 * @param array $context
Chris@0 236 *
Chris@0 237 * @return mixed
Chris@0 238 *
Chris@14 239 * @throws NotNormalizableValueException
Chris@0 240 * @throws LogicException
Chris@0 241 */
Chris@0 242 private function validateAndDenormalize($currentClass, $attribute, $data, $format, array $context)
Chris@0 243 {
Chris@0 244 if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
Chris@0 245 return $data;
Chris@0 246 }
Chris@0 247
Chris@17 248 $expectedTypes = [];
Chris@0 249 foreach ($types as $type) {
Chris@0 250 if (null === $data && $type->isNullable()) {
Chris@0 251 return;
Chris@0 252 }
Chris@0 253
Chris@0 254 if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
Chris@0 255 $builtinType = Type::BUILTIN_TYPE_OBJECT;
Chris@0 256 $class = $collectionValueType->getClassName().'[]';
Chris@0 257
Chris@16 258 // Fix a collection that contains the only one element
Chris@16 259 // This is special to xml format only
Chris@17 260 if ('xml' === $format && !\is_int(key($data))) {
Chris@17 261 $data = [$data];
Chris@16 262 }
Chris@16 263
Chris@0 264 if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
Chris@0 265 $context['key_type'] = $collectionKeyType;
Chris@0 266 }
Chris@0 267 } else {
Chris@0 268 $builtinType = $type->getBuiltinType();
Chris@0 269 $class = $type->getClassName();
Chris@0 270 }
Chris@0 271
Chris@0 272 $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
Chris@0 273
Chris@0 274 if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
Chris@0 275 if (!$this->serializer instanceof DenormalizerInterface) {
Chris@0 276 throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer', $attribute, $class));
Chris@0 277 }
Chris@0 278
Chris@14 279 $childContext = $this->createChildContext($context, $attribute);
Chris@14 280 if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) {
Chris@14 281 return $this->serializer->denormalize($data, $class, $format, $childContext);
Chris@0 282 }
Chris@0 283 }
Chris@0 284
Chris@0 285 // JSON only has a Number type corresponding to both int and float PHP types.
Chris@0 286 // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
Chris@0 287 // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
Chris@0 288 // PHP's json_decode automatically converts Numbers without a decimal part to integers.
Chris@0 289 // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
Chris@0 290 // a float is expected.
Chris@17 291 if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) {
Chris@0 292 return (float) $data;
Chris@0 293 }
Chris@0 294
Chris@17 295 if (\call_user_func('is_'.$builtinType, $data)) {
Chris@0 296 return $data;
Chris@0 297 }
Chris@0 298 }
Chris@0 299
Chris@14 300 if (!empty($context[self::DISABLE_TYPE_ENFORCEMENT])) {
Chris@14 301 return $data;
Chris@14 302 }
Chris@14 303
Chris@17 304 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 305 }
Chris@17 306
Chris@17 307 /**
Chris@17 308 * @internal
Chris@17 309 */
Chris@17 310 protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null)
Chris@17 311 {
Chris@17 312 if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) {
Chris@17 313 return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format);
Chris@17 314 }
Chris@17 315
Chris@17 316 return $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context);
Chris@0 317 }
Chris@0 318
Chris@0 319 /**
Chris@0 320 * Sets an attribute and apply the name converter if necessary.
Chris@0 321 *
Chris@0 322 * @param string $attribute
Chris@0 323 * @param mixed $attributeValue
Chris@0 324 *
Chris@0 325 * @return array
Chris@0 326 */
Chris@0 327 private function updateData(array $data, $attribute, $attributeValue)
Chris@0 328 {
Chris@0 329 if ($this->nameConverter) {
Chris@0 330 $attribute = $this->nameConverter->normalize($attribute);
Chris@0 331 }
Chris@0 332
Chris@0 333 $data[$attribute] = $attributeValue;
Chris@0 334
Chris@0 335 return $data;
Chris@0 336 }
Chris@0 337
Chris@0 338 /**
Chris@0 339 * Is the max depth reached for the given attribute?
Chris@0 340 *
Chris@0 341 * @param AttributeMetadataInterface[] $attributesMetadata
Chris@0 342 * @param string $class
Chris@0 343 * @param string $attribute
Chris@0 344 * @param array $context
Chris@0 345 *
Chris@0 346 * @return bool
Chris@0 347 */
Chris@0 348 private function isMaxDepthReached(array $attributesMetadata, $class, $attribute, array &$context)
Chris@0 349 {
Chris@0 350 if (
Chris@0 351 !isset($context[static::ENABLE_MAX_DEPTH]) ||
Chris@16 352 !$context[static::ENABLE_MAX_DEPTH] ||
Chris@0 353 !isset($attributesMetadata[$attribute]) ||
Chris@0 354 null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
Chris@0 355 ) {
Chris@0 356 return false;
Chris@0 357 }
Chris@0 358
Chris@0 359 $key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute);
Chris@0 360 if (!isset($context[$key])) {
Chris@0 361 $context[$key] = 1;
Chris@0 362
Chris@0 363 return false;
Chris@0 364 }
Chris@0 365
Chris@0 366 if ($context[$key] === $maxDepth) {
Chris@0 367 return true;
Chris@0 368 }
Chris@0 369
Chris@0 370 ++$context[$key];
Chris@0 371
Chris@0 372 return false;
Chris@0 373 }
Chris@0 374
Chris@0 375 /**
Chris@0 376 * Gets the cache key to use.
Chris@0 377 *
Chris@0 378 * @param string|null $format
Chris@0 379 * @param array $context
Chris@0 380 *
Chris@0 381 * @return bool|string
Chris@0 382 */
Chris@0 383 private function getCacheKey($format, array $context)
Chris@0 384 {
Chris@0 385 try {
Chris@0 386 return md5($format.serialize($context));
Chris@0 387 } catch (\Exception $exception) {
Chris@0 388 // The context cannot be serialized, skip the cache
Chris@0 389 return false;
Chris@0 390 }
Chris@0 391 }
Chris@0 392 }