AbstractObjectNormalizer.php 12 KB

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