comparison vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 7a779792577d
comparison
equal deleted inserted replaced
-1:000000000000 0:4c8ae668cc8c
1 <?php
2
3 /*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Symfony\Component\Serializer\Normalizer;
13
14 use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
15 use Symfony\Component\Serializer\Encoder\JsonEncoder;
16 use Symfony\Component\Serializer\Exception\CircularReferenceException;
17 use Symfony\Component\Serializer\Exception\LogicException;
18 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
19 use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
20 use Symfony\Component\PropertyInfo\Type;
21 use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
22 use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
23 use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
24
25 /**
26 * Base class for a normalizer dealing with objects.
27 *
28 * @author Kévin Dunglas <dunglas@gmail.com>
29 */
30 abstract class AbstractObjectNormalizer extends AbstractNormalizer
31 {
32 const ENABLE_MAX_DEPTH = 'enable_max_depth';
33 const DEPTH_KEY_PATTERN = 'depth_%s::%s';
34
35 private $propertyTypeExtractor;
36 private $attributesCache = array();
37
38 public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
39 {
40 parent::__construct($classMetadataFactory, $nameConverter);
41
42 $this->propertyTypeExtractor = $propertyTypeExtractor;
43 }
44
45 /**
46 * {@inheritdoc}
47 */
48 public function supportsNormalization($data, $format = null)
49 {
50 return is_object($data) && !$data instanceof \Traversable;
51 }
52
53 /**
54 * {@inheritdoc}
55 *
56 * @throws CircularReferenceException
57 */
58 public function normalize($object, $format = null, array $context = array())
59 {
60 if (!isset($context['cache_key'])) {
61 $context['cache_key'] = $this->getCacheKey($format, $context);
62 }
63
64 if ($this->isCircularReference($object, $context)) {
65 return $this->handleCircularReference($object);
66 }
67
68 $data = array();
69 $stack = array();
70 $attributes = $this->getAttributes($object, $format, $context);
71 $class = get_class($object);
72 $attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
73
74 foreach ($attributes as $attribute) {
75 if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) {
76 continue;
77 }
78
79 $attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
80
81 if (isset($this->callbacks[$attribute])) {
82 $attributeValue = call_user_func($this->callbacks[$attribute], $attributeValue);
83 }
84
85 if (null !== $attributeValue && !is_scalar($attributeValue)) {
86 $stack[$attribute] = $attributeValue;
87 }
88
89 $data = $this->updateData($data, $attribute, $attributeValue);
90 }
91
92 foreach ($stack as $attribute => $attributeValue) {
93 if (!$this->serializer instanceof NormalizerInterface) {
94 throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer', $attribute));
95 }
96
97 $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $context));
98 }
99
100 return $data;
101 }
102
103 /**
104 * Gets and caches attributes for the given object, format and context.
105 *
106 * @param object $object
107 * @param string|null $format
108 * @param array $context
109 *
110 * @return string[]
111 */
112 protected function getAttributes($object, $format = null, array $context)
113 {
114 $class = get_class($object);
115 $key = $class.'-'.$context['cache_key'];
116
117 if (isset($this->attributesCache[$key])) {
118 return $this->attributesCache[$key];
119 }
120
121 $allowedAttributes = $this->getAllowedAttributes($object, $context, true);
122
123 if (false !== $allowedAttributes) {
124 if ($context['cache_key']) {
125 $this->attributesCache[$key] = $allowedAttributes;
126 }
127
128 return $allowedAttributes;
129 }
130
131 if (isset($this->attributesCache[$class])) {
132 return $this->attributesCache[$class];
133 }
134
135 return $this->attributesCache[$class] = $this->extractAttributes($object, $format, $context);
136 }
137
138 /**
139 * Extracts attributes to normalize from the class of the given object, format and context.
140 *
141 * @param object $object
142 * @param string|null $format
143 * @param array $context
144 *
145 * @return string[]
146 */
147 abstract protected function extractAttributes($object, $format = null, array $context = array());
148
149 /**
150 * Gets the attribute value.
151 *
152 * @param object $object
153 * @param string $attribute
154 * @param string|null $format
155 * @param array $context
156 *
157 * @return mixed
158 */
159 abstract protected function getAttributeValue($object, $attribute, $format = null, array $context = array());
160
161 /**
162 * {@inheritdoc}
163 */
164 public function supportsDenormalization($data, $type, $format = null)
165 {
166 return class_exists($type);
167 }
168
169 /**
170 * {@inheritdoc}
171 */
172 public function denormalize($data, $class, $format = null, array $context = array())
173 {
174 if (!isset($context['cache_key'])) {
175 $context['cache_key'] = $this->getCacheKey($format, $context);
176 }
177 $allowedAttributes = $this->getAllowedAttributes($class, $context, true);
178 $normalizedData = $this->prepareForDenormalization($data);
179
180 $reflectionClass = new \ReflectionClass($class);
181 $object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes, $format);
182
183 foreach ($normalizedData as $attribute => $value) {
184 if ($this->nameConverter) {
185 $attribute = $this->nameConverter->denormalize($attribute);
186 }
187
188 if (($allowedAttributes !== false && !in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($class, $attribute, $format, $context)) {
189 continue;
190 }
191
192 $value = $this->validateAndDenormalize($class, $attribute, $value, $format, $context);
193 try {
194 $this->setAttributeValue($object, $attribute, $value, $format, $context);
195 } catch (InvalidArgumentException $e) {
196 throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
197 }
198 }
199
200 return $object;
201 }
202
203 /**
204 * Sets attribute value.
205 *
206 * @param object $object
207 * @param string $attribute
208 * @param mixed $value
209 * @param string|null $format
210 * @param array $context
211 */
212 abstract protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array());
213
214 /**
215 * Validates the submitted data and denormalizes it.
216 *
217 * @param string $currentClass
218 * @param string $attribute
219 * @param mixed $data
220 * @param string|null $format
221 * @param array $context
222 *
223 * @return mixed
224 *
225 * @throws UnexpectedValueException
226 * @throws LogicException
227 */
228 private function validateAndDenormalize($currentClass, $attribute, $data, $format, array $context)
229 {
230 if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
231 return $data;
232 }
233
234 $expectedTypes = array();
235 foreach ($types as $type) {
236 if (null === $data && $type->isNullable()) {
237 return;
238 }
239
240 if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
241 $builtinType = Type::BUILTIN_TYPE_OBJECT;
242 $class = $collectionValueType->getClassName().'[]';
243
244 if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
245 $context['key_type'] = $collectionKeyType;
246 }
247 } else {
248 $builtinType = $type->getBuiltinType();
249 $class = $type->getClassName();
250 }
251
252 $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
253
254 if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
255 if (!$this->serializer instanceof DenormalizerInterface) {
256 throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer', $attribute, $class));
257 }
258
259 if ($this->serializer->supportsDenormalization($data, $class, $format)) {
260 return $this->serializer->denormalize($data, $class, $format, $context);
261 }
262 }
263
264 // JSON only has a Number type corresponding to both int and float PHP types.
265 // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
266 // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
267 // PHP's json_decode automatically converts Numbers without a decimal part to integers.
268 // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
269 // a float is expected.
270 if (Type::BUILTIN_TYPE_FLOAT === $builtinType && is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) {
271 return (float) $data;
272 }
273
274 if (call_user_func('is_'.$builtinType, $data)) {
275 return $data;
276 }
277 }
278
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)));
280 }
281
282 /**
283 * Sets an attribute and apply the name converter if necessary.
284 *
285 * @param array $data
286 * @param string $attribute
287 * @param mixed $attributeValue
288 *
289 * @return array
290 */
291 private function updateData(array $data, $attribute, $attributeValue)
292 {
293 if ($this->nameConverter) {
294 $attribute = $this->nameConverter->normalize($attribute);
295 }
296
297 $data[$attribute] = $attributeValue;
298
299 return $data;
300 }
301
302 /**
303 * Is the max depth reached for the given attribute?
304 *
305 * @param AttributeMetadataInterface[] $attributesMetadata
306 * @param string $class
307 * @param string $attribute
308 * @param array $context
309 *
310 * @return bool
311 */
312 private function isMaxDepthReached(array $attributesMetadata, $class, $attribute, array &$context)
313 {
314 if (
315 !isset($context[static::ENABLE_MAX_DEPTH]) ||
316 !isset($attributesMetadata[$attribute]) ||
317 null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
318 ) {
319 return false;
320 }
321
322 $key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute);
323 if (!isset($context[$key])) {
324 $context[$key] = 1;
325
326 return false;
327 }
328
329 if ($context[$key] === $maxDepth) {
330 return true;
331 }
332
333 ++$context[$key];
334
335 return false;
336 }
337
338 /**
339 * Gets the cache key to use.
340 *
341 * @param string|null $format
342 * @param array $context
343 *
344 * @return bool|string
345 */
346 private function getCacheKey($format, array $context)
347 {
348 try {
349 return md5($format.serialize($context));
350 } catch (\Exception $exception) {
351 // The context cannot be serialized, skip the cache
352 return false;
353 }
354 }
355 }