vendor/api-platform/core/src/Core/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php line 105

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the API Platform project.
  4. *
  5. * (c) Kévin Dunglas <dunglas@gmail.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. declare(strict_types=1);
  11. namespace ApiPlatform\Core\Metadata\Property\Factory;
  12. use ApiPlatform\Core\Api\ResourceClassResolverInterface;
  13. use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
  14. use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
  15. use ApiPlatform\Exception\ResourceClassNotFoundException;
  16. use ApiPlatform\Util\ResourceClassInfoTrait;
  17. use Symfony\Component\PropertyInfo\Type;
  18. use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
  19. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface as SerializerClassMetadataFactoryInterface;
  20. /**
  21. * Populates read/write and link status using serialization groups.
  22. *
  23. * @author Kévin Dunglas <dunglas@gmail.com>
  24. * @author Teoh Han Hui <teohhanhui@gmail.com>
  25. */
  26. final class SerializerPropertyMetadataFactory implements PropertyMetadataFactoryInterface
  27. {
  28. use ResourceClassInfoTrait;
  29. private $serializerClassMetadataFactory;
  30. private $decorated;
  31. public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, SerializerClassMetadataFactoryInterface $serializerClassMetadataFactory, PropertyMetadataFactoryInterface $decorated, ResourceClassResolverInterface $resourceClassResolver = null)
  32. {
  33. $this->resourceMetadataFactory = $resourceMetadataFactory;
  34. $this->serializerClassMetadataFactory = $serializerClassMetadataFactory;
  35. $this->decorated = $decorated;
  36. $this->resourceClassResolver = $resourceClassResolver;
  37. }
  38. /**
  39. * {@inheritdoc}
  40. */
  41. public function create(string $resourceClass, string $property, array $options = []): PropertyMetadata
  42. {
  43. $propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
  44. // BC to be removed in 3.0
  45. if (null !== ($childResourceClass = $propertyMetadata->getChildInherited())) {
  46. $resourceClass = $childResourceClass;
  47. }
  48. try {
  49. [$normalizationGroups, $denormalizationGroups] = $this->getEffectiveSerializerGroups($options, $resourceClass);
  50. } catch (ResourceClassNotFoundException $e) {
  51. // TODO: for input/output classes, the serializer groups must be read from the actual resource class
  52. return $propertyMetadata;
  53. }
  54. $propertyMetadata = $this->transformReadWrite($propertyMetadata, $resourceClass, $property, $normalizationGroups, $denormalizationGroups);
  55. return $this->transformLinkStatus($propertyMetadata, $normalizationGroups, $denormalizationGroups);
  56. }
  57. /**
  58. * Sets readable/writable based on matching normalization/denormalization groups and property's ignorance.
  59. *
  60. * A false value is never reset as it could be unreadable/unwritable for other reasons.
  61. * If normalization/denormalization groups are not specified and the property is not ignored, the property is implicitly readable/writable.
  62. *
  63. * @param string[]|null $normalizationGroups
  64. * @param string[]|null $denormalizationGroups
  65. */
  66. private function transformReadWrite(PropertyMetadata $propertyMetadata, string $resourceClass, string $propertyName, array $normalizationGroups = null, array $denormalizationGroups = null): PropertyMetadata
  67. {
  68. $serializerAttributeMetadata = $this->getSerializerAttributeMetadata($resourceClass, $propertyName);
  69. $groups = $serializerAttributeMetadata ? $serializerAttributeMetadata->getGroups() : [];
  70. $ignored = $serializerAttributeMetadata && method_exists($serializerAttributeMetadata, 'isIgnored') ? $serializerAttributeMetadata->isIgnored() : false;
  71. if (false !== $propertyMetadata->isReadable()) {
  72. $propertyMetadata = $propertyMetadata->withReadable(!$ignored && (null === $normalizationGroups || array_intersect($normalizationGroups, $groups)));
  73. }
  74. if (false !== $propertyMetadata->isWritable()) {
  75. $propertyMetadata = $propertyMetadata->withWritable(!$ignored && (null === $denormalizationGroups || array_intersect($denormalizationGroups, $groups)));
  76. }
  77. return $propertyMetadata;
  78. }
  79. /**
  80. * Sets readableLink/writableLink based on matching normalization/denormalization groups.
  81. *
  82. * If normalization/denormalization groups are not specified,
  83. * set link status to false since embedding of resource must be explicitly enabled
  84. *
  85. * @param string[]|null $normalizationGroups
  86. * @param string[]|null $denormalizationGroups
  87. */
  88. private function transformLinkStatus(PropertyMetadata $propertyMetadata, array $normalizationGroups = null, array $denormalizationGroups = null): PropertyMetadata
  89. {
  90. // No need to check link status if property is not readable and not writable
  91. if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
  92. return $propertyMetadata;
  93. }
  94. $type = $propertyMetadata->getType();
  95. if (null === $type) {
  96. return $propertyMetadata;
  97. }
  98. if (
  99. $type->isCollection() &&
  100. $collectionValueType = method_exists(Type::class, 'getCollectionValueTypes') ? ($type->getCollectionValueTypes()[0] ?? null) : $type->getCollectionValueType()
  101. ) {
  102. $relatedClass = $collectionValueType->getClassName();
  103. } else {
  104. $relatedClass = $type->getClassName();
  105. }
  106. // if property is not a resource relation, don't set link status (as it would have no meaning)
  107. if (null === $relatedClass || !$this->isResourceClass($relatedClass)) {
  108. return $propertyMetadata;
  109. }
  110. // find the resource class
  111. // this prevents serializer groups on non-resource child class from incorrectly influencing the decision
  112. if (null !== $this->resourceClassResolver) {
  113. $relatedClass = $this->resourceClassResolver->getResourceClass(null, $relatedClass);
  114. }
  115. $relatedGroups = $this->getClassSerializerGroups($relatedClass);
  116. if (null === $propertyMetadata->isReadableLink()) {
  117. $propertyMetadata = $propertyMetadata->withReadableLink(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups)));
  118. }
  119. if (null === $propertyMetadata->isWritableLink()) {
  120. $propertyMetadata = $propertyMetadata->withWritableLink(null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups)));
  121. }
  122. return $propertyMetadata;
  123. }
  124. /**
  125. * Gets the effective serializer groups used in normalization/denormalization.
  126. *
  127. * Groups are extracted in the following order:
  128. *
  129. * - From the "serializer_groups" key of the $options array.
  130. * - From metadata of the given operation ("collection_operation_name" and "item_operation_name" keys).
  131. * - From metadata of the current resource.
  132. *
  133. * @throws ResourceClassNotFoundException
  134. *
  135. * @return (string[]|null)[]
  136. */
  137. private function getEffectiveSerializerGroups(array $options, string $resourceClass): array
  138. {
  139. if (isset($options['serializer_groups'])) {
  140. $groups = (array) $options['serializer_groups'];
  141. return [$groups, $groups];
  142. }
  143. if (\array_key_exists('normalization_groups', $options) && \array_key_exists('denormalization_groups', $options)) {
  144. return [$options['normalization_groups'] ?? null, $options['denormalization_groups'] ?? null];
  145. }
  146. $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
  147. if (isset($options['collection_operation_name'])) {
  148. $normalizationContext = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'normalization_context', null, true);
  149. $denormalizationContext = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'denormalization_context', null, true);
  150. } elseif (isset($options['item_operation_name'])) {
  151. $normalizationContext = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'normalization_context', null, true);
  152. $denormalizationContext = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'denormalization_context', null, true);
  153. } elseif (isset($options['graphql_operation_name'])) {
  154. $normalizationContext = $resourceMetadata->getGraphqlAttribute($options['graphql_operation_name'], 'normalization_context', null, true);
  155. $denormalizationContext = $resourceMetadata->getGraphqlAttribute($options['graphql_operation_name'], 'denormalization_context', null, true);
  156. } else {
  157. $normalizationContext = $resourceMetadata->getAttribute('normalization_context');
  158. $denormalizationContext = $resourceMetadata->getAttribute('denormalization_context');
  159. }
  160. return [
  161. isset($normalizationContext['groups']) ? (array) $normalizationContext['groups'] : null,
  162. isset($denormalizationContext['groups']) ? (array) $denormalizationContext['groups'] : null,
  163. ];
  164. }
  165. private function getSerializerAttributeMetadata(string $class, string $attribute): ?AttributeMetadataInterface
  166. {
  167. $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class);
  168. foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
  169. if ($attribute === $serializerAttributeMetadata->getName()) {
  170. return $serializerAttributeMetadata;
  171. }
  172. }
  173. return null;
  174. }
  175. /**
  176. * Gets all serializer groups used in a class.
  177. *
  178. * @return string[]
  179. */
  180. private function getClassSerializerGroups(string $class): array
  181. {
  182. try {
  183. $resourceMetadata = $this->resourceMetadataFactory->create($class);
  184. if ($outputClass = $resourceMetadata->getAttribute('output')['class'] ?? null) {
  185. $class = $outputClass;
  186. }
  187. } catch (ResourceClassNotFoundException $e) {
  188. }
  189. $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class);
  190. $groups = [];
  191. foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
  192. $groups = array_merge($groups, $serializerAttributeMetadata->getGroups());
  193. }
  194. return array_unique($groups);
  195. }
  196. }