annotate vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php @ 8:50b0d041100e

Further files for download
author Chris Cannam
date Mon, 05 Feb 2018 10:56:40 +0000
parents 4c8ae668cc8c
children 7a779792577d
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@0 15 use Symfony\Component\Serializer\Encoder\JsonEncoder;
Chris@0 16 use Symfony\Component\Serializer\Exception\CircularReferenceException;
Chris@0 17 use Symfony\Component\Serializer\Exception\LogicException;
Chris@0 18 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
Chris@0 19 use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
Chris@0 20 use Symfony\Component\PropertyInfo\Type;
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@0 34
Chris@0 35 private $propertyTypeExtractor;
Chris@0 36 private $attributesCache = array();
Chris@0 37
Chris@0 38 public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
Chris@0 39 {
Chris@0 40 parent::__construct($classMetadataFactory, $nameConverter);
Chris@0 41
Chris@0 42 $this->propertyTypeExtractor = $propertyTypeExtractor;
Chris@0 43 }
Chris@0 44
Chris@0 45 /**
Chris@0 46 * {@inheritdoc}
Chris@0 47 */
Chris@0 48 public function supportsNormalization($data, $format = null)
Chris@0 49 {
Chris@0 50 return is_object($data) && !$data instanceof \Traversable;
Chris@0 51 }
Chris@0 52
Chris@0 53 /**
Chris@0 54 * {@inheritdoc}
Chris@0 55 *
Chris@0 56 * @throws CircularReferenceException
Chris@0 57 */
Chris@0 58 public function normalize($object, $format = null, array $context = array())
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@0 68 $data = array();
Chris@0 69 $stack = array();
Chris@0 70 $attributes = $this->getAttributes($object, $format, $context);
Chris@0 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@0 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@0 97 $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $context));
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@0 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@0 131 if (isset($this->attributesCache[$class])) {
Chris@0 132 return $this->attributesCache[$class];
Chris@0 133 }
Chris@0 134
Chris@0 135 return $this->attributesCache[$class] = $this->extractAttributes($object, $format, $context);
Chris@0 136 }
Chris@0 137
Chris@0 138 /**
Chris@0 139 * Extracts attributes to normalize from the class of the given object, format and context.
Chris@0 140 *
Chris@0 141 * @param object $object
Chris@0 142 * @param string|null $format
Chris@0 143 * @param array $context
Chris@0 144 *
Chris@0 145 * @return string[]
Chris@0 146 */
Chris@0 147 abstract protected function extractAttributes($object, $format = null, array $context = array());
Chris@0 148
Chris@0 149 /**
Chris@0 150 * Gets the attribute value.
Chris@0 151 *
Chris@0 152 * @param object $object
Chris@0 153 * @param string $attribute
Chris@0 154 * @param string|null $format
Chris@0 155 * @param array $context
Chris@0 156 *
Chris@0 157 * @return mixed
Chris@0 158 */
Chris@0 159 abstract protected function getAttributeValue($object, $attribute, $format = null, array $context = array());
Chris@0 160
Chris@0 161 /**
Chris@0 162 * {@inheritdoc}
Chris@0 163 */
Chris@0 164 public function supportsDenormalization($data, $type, $format = null)
Chris@0 165 {
Chris@0 166 return class_exists($type);
Chris@0 167 }
Chris@0 168
Chris@0 169 /**
Chris@0 170 * {@inheritdoc}
Chris@0 171 */
Chris@0 172 public function denormalize($data, $class, $format = null, array $context = array())
Chris@0 173 {
Chris@0 174 if (!isset($context['cache_key'])) {
Chris@0 175 $context['cache_key'] = $this->getCacheKey($format, $context);
Chris@0 176 }
Chris@0 177 $allowedAttributes = $this->getAllowedAttributes($class, $context, true);
Chris@0 178 $normalizedData = $this->prepareForDenormalization($data);
Chris@0 179
Chris@0 180 $reflectionClass = new \ReflectionClass($class);
Chris@0 181 $object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format);
Chris@0 182
Chris@0 183 foreach ($normalizedData as $attribute => $value) {
Chris@0 184 if ($this->nameConverter) {
Chris@0 185 $attribute = $this->nameConverter->denormalize($attribute);
Chris@0 186 }
Chris@0 187
Chris@0 188 if (($allowedAttributes !== false && !in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($class, $attribute, $format, $context)) {
Chris@0 189 continue;
Chris@0 190 }
Chris@0 191
Chris@0 192 $value = $this->validateAndDenormalize($class, $attribute, $value, $format, $context);
Chris@0 193 try {
Chris@0 194 $this->setAttributeValue($object, $attribute, $value, $format, $context);
Chris@0 195 } catch (InvalidArgumentException $e) {
Chris@0 196 throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
Chris@0 197 }
Chris@0 198 }
Chris@0 199
Chris@0 200 return $object;
Chris@0 201 }
Chris@0 202
Chris@0 203 /**
Chris@0 204 * Sets attribute value.
Chris@0 205 *
Chris@0 206 * @param object $object
Chris@0 207 * @param string $attribute
Chris@0 208 * @param mixed $value
Chris@0 209 * @param string|null $format
Chris@0 210 * @param array $context
Chris@0 211 */
Chris@0 212 abstract protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array());
Chris@0 213
Chris@0 214 /**
Chris@0 215 * Validates the submitted data and denormalizes it.
Chris@0 216 *
Chris@0 217 * @param string $currentClass
Chris@0 218 * @param string $attribute
Chris@0 219 * @param mixed $data
Chris@0 220 * @param string|null $format
Chris@0 221 * @param array $context
Chris@0 222 *
Chris@0 223 * @return mixed
Chris@0 224 *
Chris@0 225 * @throws UnexpectedValueException
Chris@0 226 * @throws LogicException
Chris@0 227 */
Chris@0 228 private function validateAndDenormalize($currentClass, $attribute, $data, $format, array $context)
Chris@0 229 {
Chris@0 230 if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
Chris@0 231 return $data;
Chris@0 232 }
Chris@0 233
Chris@0 234 $expectedTypes = array();
Chris@0 235 foreach ($types as $type) {
Chris@0 236 if (null === $data && $type->isNullable()) {
Chris@0 237 return;
Chris@0 238 }
Chris@0 239
Chris@0 240 if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
Chris@0 241 $builtinType = Type::BUILTIN_TYPE_OBJECT;
Chris@0 242 $class = $collectionValueType->getClassName().'[]';
Chris@0 243
Chris@0 244 if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
Chris@0 245 $context['key_type'] = $collectionKeyType;
Chris@0 246 }
Chris@0 247 } else {
Chris@0 248 $builtinType = $type->getBuiltinType();
Chris@0 249 $class = $type->getClassName();
Chris@0 250 }
Chris@0 251
Chris@0 252 $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
Chris@0 253
Chris@0 254 if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
Chris@0 255 if (!$this->serializer instanceof DenormalizerInterface) {
Chris@0 256 throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer', $attribute, $class));
Chris@0 257 }
Chris@0 258
Chris@0 259 if ($this->serializer->supportsDenormalization($data, $class, $format)) {
Chris@0 260 return $this->serializer->denormalize($data, $class, $format, $context);
Chris@0 261 }
Chris@0 262 }
Chris@0 263
Chris@0 264 // JSON only has a Number type corresponding to both int and float PHP types.
Chris@0 265 // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
Chris@0 266 // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
Chris@0 267 // PHP's json_decode automatically converts Numbers without a decimal part to integers.
Chris@0 268 // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
Chris@0 269 // a float is expected.
Chris@0 270 if (Type::BUILTIN_TYPE_FLOAT === $builtinType && is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) {
Chris@0 271 return (float) $data;
Chris@0 272 }
Chris@0 273
Chris@0 274 if (call_user_func('is_'.$builtinType, $data)) {
Chris@0 275 return $data;
Chris@0 276 }
Chris@0 277 }
Chris@0 278
Chris@0 279 throw new UnexpectedValueException(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@0 280 }
Chris@0 281
Chris@0 282 /**
Chris@0 283 * Sets an attribute and apply the name converter if necessary.
Chris@0 284 *
Chris@0 285 * @param array $data
Chris@0 286 * @param string $attribute
Chris@0 287 * @param mixed $attributeValue
Chris@0 288 *
Chris@0 289 * @return array
Chris@0 290 */
Chris@0 291 private function updateData(array $data, $attribute, $attributeValue)
Chris@0 292 {
Chris@0 293 if ($this->nameConverter) {
Chris@0 294 $attribute = $this->nameConverter->normalize($attribute);
Chris@0 295 }
Chris@0 296
Chris@0 297 $data[$attribute] = $attributeValue;
Chris@0 298
Chris@0 299 return $data;
Chris@0 300 }
Chris@0 301
Chris@0 302 /**
Chris@0 303 * Is the max depth reached for the given attribute?
Chris@0 304 *
Chris@0 305 * @param AttributeMetadataInterface[] $attributesMetadata
Chris@0 306 * @param string $class
Chris@0 307 * @param string $attribute
Chris@0 308 * @param array $context
Chris@0 309 *
Chris@0 310 * @return bool
Chris@0 311 */
Chris@0 312 private function isMaxDepthReached(array $attributesMetadata, $class, $attribute, array &$context)
Chris@0 313 {
Chris@0 314 if (
Chris@0 315 !isset($context[static::ENABLE_MAX_DEPTH]) ||
Chris@0 316 !isset($attributesMetadata[$attribute]) ||
Chris@0 317 null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
Chris@0 318 ) {
Chris@0 319 return false;
Chris@0 320 }
Chris@0 321
Chris@0 322 $key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute);
Chris@0 323 if (!isset($context[$key])) {
Chris@0 324 $context[$key] = 1;
Chris@0 325
Chris@0 326 return false;
Chris@0 327 }
Chris@0 328
Chris@0 329 if ($context[$key] === $maxDepth) {
Chris@0 330 return true;
Chris@0 331 }
Chris@0 332
Chris@0 333 ++$context[$key];
Chris@0 334
Chris@0 335 return false;
Chris@0 336 }
Chris@0 337
Chris@0 338 /**
Chris@0 339 * Gets the cache key to use.
Chris@0 340 *
Chris@0 341 * @param string|null $format
Chris@0 342 * @param array $context
Chris@0 343 *
Chris@0 344 * @return bool|string
Chris@0 345 */
Chris@0 346 private function getCacheKey($format, array $context)
Chris@0 347 {
Chris@0 348 try {
Chris@0 349 return md5($format.serialize($context));
Chris@0 350 } catch (\Exception $exception) {
Chris@0 351 // The context cannot be serialized, skip the cache
Chris@0 352 return false;
Chris@0 353 }
Chris@0 354 }
Chris@0 355 }