ObjectNormalizer.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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\NoSuchPropertyException;
  12. use Symfony\Component\PropertyAccess\PropertyAccess;
  13. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  14. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  15. use Symfony\Component\Serializer\Exception\LogicException;
  16. use Symfony\Component\Serializer\Mapping\AttributeMetadata;
  17. use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
  18. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
  19. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  20. /**
  21. * Converts between objects and arrays using the PropertyAccess component.
  22. *
  23. * @author Kévin Dunglas <dunglas@gmail.com>
  24. */
  25. class ObjectNormalizer extends AbstractObjectNormalizer
  26. {
  27. protected $propertyAccessor;
  28. private $discriminatorCache = [];
  29. private $objectClassResolver;
  30. public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [])
  31. {
  32. if (!class_exists(PropertyAccess::class)) {
  33. throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.');
  34. }
  35. parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext);
  36. $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
  37. $this->objectClassResolver = $objectClassResolver ?? function ($class) {
  38. return \is_object($class) ? \get_class($class) : $class;
  39. };
  40. }
  41. /**
  42. * {@inheritdoc}
  43. */
  44. public function hasCacheableSupportsMethod(): bool
  45. {
  46. return __CLASS__ === static::class;
  47. }
  48. /**
  49. * {@inheritdoc}
  50. */
  51. protected function extractAttributes(object $object, string $format = null, array $context = [])
  52. {
  53. if (\stdClass::class === \get_class($object)) {
  54. return array_keys((array) $object);
  55. }
  56. // If not using groups, detect manually
  57. $attributes = [];
  58. // methods
  59. $class = ($this->objectClassResolver)($object);
  60. $reflClass = new \ReflectionClass($class);
  61. foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) {
  62. if (
  63. 0 !== $reflMethod->getNumberOfRequiredParameters() ||
  64. $reflMethod->isStatic() ||
  65. $reflMethod->isConstructor() ||
  66. $reflMethod->isDestructor()
  67. ) {
  68. continue;
  69. }
  70. $name = $reflMethod->name;
  71. $attributeName = null;
  72. if (0 === strpos($name, 'get') || 0 === strpos($name, 'has')) {
  73. // getters and hassers
  74. $attributeName = substr($name, 3);
  75. if (!$reflClass->hasProperty($attributeName)) {
  76. $attributeName = lcfirst($attributeName);
  77. }
  78. } elseif (0 === strpos($name, 'is')) {
  79. // issers
  80. $attributeName = substr($name, 2);
  81. if (!$reflClass->hasProperty($attributeName)) {
  82. $attributeName = lcfirst($attributeName);
  83. }
  84. }
  85. if (null !== $attributeName && $this->isAllowedAttribute($object, $attributeName, $format, $context)) {
  86. $attributes[$attributeName] = true;
  87. }
  88. }
  89. $checkPropertyInitialization = \PHP_VERSION_ID >= 70400;
  90. // properties
  91. foreach ($reflClass->getProperties() as $reflProperty) {
  92. $isPublic = $reflProperty->isPublic();
  93. if ($checkPropertyInitialization) {
  94. if (!$isPublic) {
  95. $reflProperty->setAccessible(true);
  96. }
  97. if (!$reflProperty->isInitialized($object)) {
  98. unset($attributes[$reflProperty->name]);
  99. continue;
  100. }
  101. }
  102. if (!$isPublic) {
  103. continue;
  104. }
  105. if ($reflProperty->isStatic() || !$this->isAllowedAttribute($object, $reflProperty->name, $format, $context)) {
  106. continue;
  107. }
  108. $attributes[$reflProperty->name] = true;
  109. }
  110. return array_keys($attributes);
  111. }
  112. /**
  113. * {@inheritdoc}
  114. */
  115. protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = [])
  116. {
  117. $cacheKey = \get_class($object);
  118. if (!\array_key_exists($cacheKey, $this->discriminatorCache)) {
  119. $this->discriminatorCache[$cacheKey] = null;
  120. if (null !== $this->classDiscriminatorResolver) {
  121. $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object);
  122. $this->discriminatorCache[$cacheKey] = null === $mapping ? null : $mapping->getTypeProperty();
  123. }
  124. }
  125. return $attribute === $this->discriminatorCache[$cacheKey] ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) : $this->propertyAccessor->getValue($object, $attribute);
  126. }
  127. /**
  128. * {@inheritdoc}
  129. */
  130. protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = [])
  131. {
  132. try {
  133. $this->propertyAccessor->setValue($object, $attribute, $value);
  134. } catch (NoSuchPropertyException $exception) {
  135. // Properties not found are ignored
  136. }
  137. }
  138. /**
  139. * {@inheritdoc}
  140. */
  141. protected function getAllowedAttributes($classOrObject, array $context, bool $attributesAsString = false)
  142. {
  143. if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) {
  144. return false;
  145. }
  146. if (null !== $this->classDiscriminatorResolver) {
  147. $class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject;
  148. if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) {
  149. $allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty());
  150. }
  151. if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
  152. foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
  153. $allowedAttributes = array_merge($allowedAttributes, parent::getAllowedAttributes($mappedClass, $context, $attributesAsString));
  154. }
  155. }
  156. }
  157. return $allowedAttributes;
  158. }
  159. }