vendor/api-platform/core/src/Core/Operation/Factory/SubresourceOperationFactory.php line 81

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\Operation\Factory;
  12. use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
  13. use ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameGenerator;
  14. use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
  15. use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
  16. use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
  17. use ApiPlatform\Exception\ResourceClassNotFoundException;
  18. use ApiPlatform\Operation\PathSegmentNameGeneratorInterface;
  19. /**
  20. * @internal
  21. */
  22. final class SubresourceOperationFactory implements SubresourceOperationFactoryInterface
  23. {
  24. public const SUBRESOURCE_SUFFIX = '_subresource';
  25. public const FORMAT_SUFFIX = '.{_format}';
  26. public const ROUTE_OPTIONS = ['defaults' => [], 'requirements' => [], 'options' => [], 'host' => '', 'schemes' => [], 'condition' => '', 'controller' => null, 'stateless' => null];
  27. private $resourceMetadataFactory;
  28. private $propertyNameCollectionFactory;
  29. private $propertyMetadataFactory;
  30. private $pathSegmentNameGenerator;
  31. private $identifiersExtractor;
  32. public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PathSegmentNameGeneratorInterface $pathSegmentNameGenerator, IdentifiersExtractorInterface $identifiersExtractor = null)
  33. {
  34. $this->resourceMetadataFactory = $resourceMetadataFactory;
  35. $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
  36. $this->propertyMetadataFactory = $propertyMetadataFactory;
  37. $this->pathSegmentNameGenerator = $pathSegmentNameGenerator;
  38. $this->identifiersExtractor = $identifiersExtractor;
  39. }
  40. public function create(string $resourceClass): array
  41. {
  42. $tree = [];
  43. try {
  44. $this->computeSubresourceOperations($resourceClass, $tree);
  45. } catch (ResourceClassNotFoundException $e) {
  46. return [];
  47. }
  48. return $tree;
  49. }
  50. /**
  51. * Handles subresource operations recursively and declare their corresponding routes.
  52. *
  53. * @param string $rootResourceClass null on the first iteration, it then keeps track of the origin resource class
  54. * @param array $parentOperation the previous call operation
  55. * @param int $depth the number of visited
  56. */
  57. private function computeSubresourceOperations(string $resourceClass, array &$tree, string $rootResourceClass = null, array $parentOperation = null, array $visited = [], int $depth = 0, int $maxDepth = null): void
  58. {
  59. if (null === $rootResourceClass) {
  60. $rootResourceClass = $resourceClass;
  61. }
  62. foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
  63. $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, ['deprecate' => false]);
  64. if (!$subresource = $propertyMetadata->getSubresource()) {
  65. continue;
  66. }
  67. trigger_deprecation('api-platform/core', '2.7', sprintf('A subresource is declared on "%s::%s". Subresources are deprecated, use another #[ApiResource] instead.', $resourceClass, $property));
  68. $subresourceClass = $subresource->getResourceClass();
  69. $subresourceMetadata = $this->resourceMetadataFactory->create($subresourceClass);
  70. $subresourceMetadata = $subresourceMetadata->withAttributes(($subresourceMetadata->getAttributes() ?: []) + ['identifiers' => !$this->identifiersExtractor ? [$property] : $this->identifiersExtractor->getIdentifiersFromResourceClass($subresourceClass)]);
  71. $isLastItem = ($parentOperation['resource_class'] ?? null) === $resourceClass && $propertyMetadata->isIdentifier();
  72. // A subresource that is also an identifier can't be a start point
  73. if ($isLastItem && (null === $parentOperation || false === $parentOperation['collection'])) {
  74. continue;
  75. }
  76. $visiting = "$resourceClass $property $subresourceClass";
  77. // Handle maxDepth
  78. if (null !== $maxDepth && $depth >= $maxDepth) {
  79. break;
  80. }
  81. if (isset($visited[$visiting])) {
  82. continue;
  83. }
  84. $rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass);
  85. $rootResourceMetadata = $rootResourceMetadata->withAttributes(($rootResourceMetadata->getAttributes() ?: []) + ['identifiers' => !$this->identifiersExtractor ? ['id'] : $this->identifiersExtractor->getIdentifiersFromResourceClass($rootResourceClass)]);
  86. $operationName = 'get';
  87. $operation = [
  88. 'property' => $property,
  89. 'collection' => $subresource->isCollection(),
  90. 'resource_class' => $subresourceClass,
  91. 'shortNames' => [$subresourceMetadata->getShortName()],
  92. 'legacy_filters' => $subresourceMetadata->getAttribute('filters', []),
  93. 'legacy_normalization_context' => $subresourceMetadata->getAttribute('normalization_context', []),
  94. 'legacy_type' => $subresourceMetadata->getIri(),
  95. ];
  96. if (null === $parentOperation) {
  97. $identifiers = (array) $rootResourceMetadata->getAttribute('identifiers');
  98. $rootShortname = $rootResourceMetadata->getShortName();
  99. $identifier = \is_string($key = array_key_first($identifiers)) ? $key : $identifiers[0];
  100. $operation['identifiers'][$identifier] = [$rootResourceClass, $identifiers[$identifier][1] ?? $identifier, true];
  101. $operation['operation_name'] = sprintf(
  102. '%s_%s%s',
  103. RouteNameGenerator::inflector($operation['property'], $operation['collection']),
  104. $operationName,
  105. self::SUBRESOURCE_SUFFIX
  106. );
  107. $subresourceOperation = $rootResourceMetadata->getSubresourceOperations()[$operation['operation_name']] ?? [];
  108. $operation['route_name'] = sprintf(
  109. '%s%s_%s',
  110. RouteNameGenerator::ROUTE_NAME_PREFIX,
  111. RouteNameGenerator::inflector($rootShortname),
  112. $operation['operation_name']
  113. );
  114. $prefix = trim(trim($rootResourceMetadata->getAttribute('route_prefix', '')), '/');
  115. if ('' !== $prefix) {
  116. $prefix .= '/';
  117. }
  118. $operation['path'] = $subresourceOperation['path'] ?? sprintf(
  119. '/%s%s/{%s}/%s%s',
  120. $prefix,
  121. $this->pathSegmentNameGenerator->getSegmentName($rootShortname),
  122. $identifier,
  123. $this->pathSegmentNameGenerator->getSegmentName($operation['property'], $operation['collection']),
  124. self::FORMAT_SUFFIX
  125. );
  126. if (!\in_array($rootShortname, $operation['shortNames'], true)) {
  127. $operation['shortNames'][] = $rootShortname;
  128. }
  129. } else {
  130. $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
  131. $identifiers = (array) $resourceMetadata->getAttribute('identifiers', null === $this->identifiersExtractor ? ['id'] : $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass));
  132. $identifier = \is_string($key = array_key_first($identifiers)) ? $key : $identifiers[0];
  133. $operation['identifiers'] = $parentOperation['identifiers'];
  134. if (!isset($operation['identifiers'][$parentOperation['property']])) {
  135. $operation['identifiers'][$parentOperation['property']] = [$resourceClass, $identifiers[$identifier][1] ?? $identifier, $isLastItem ? true : $parentOperation['collection']];
  136. }
  137. $operation['operation_name'] = str_replace(
  138. 'get'.self::SUBRESOURCE_SUFFIX,
  139. RouteNameGenerator::inflector($isLastItem ? 'item' : $property, $operation['collection']).'_get'.self::SUBRESOURCE_SUFFIX,
  140. $parentOperation['operation_name']
  141. );
  142. $operation['route_name'] = str_replace($parentOperation['operation_name'], $operation['operation_name'], $parentOperation['route_name']);
  143. if (!\in_array($resourceMetadata->getShortName(), $operation['shortNames'], true)) {
  144. $operation['shortNames'][] = $resourceMetadata->getShortName();
  145. }
  146. $subresourceOperation = $rootResourceMetadata->getSubresourceOperations()[$operation['operation_name']] ?? [];
  147. if (isset($subresourceOperation['path'])) {
  148. $operation['path'] = $subresourceOperation['path'];
  149. } else {
  150. $operation['path'] = str_replace(self::FORMAT_SUFFIX, '', (string) $parentOperation['path']);
  151. if ($parentOperation['collection']) {
  152. $operation['path'] .= sprintf('/{%s}', array_key_last($operation['identifiers']));
  153. }
  154. if ($isLastItem) {
  155. $operation['path'] .= self::FORMAT_SUFFIX;
  156. } else {
  157. $operation['path'] .= sprintf('/%s%s', $this->pathSegmentNameGenerator->getSegmentName($property, $operation['collection']), self::FORMAT_SUFFIX);
  158. }
  159. }
  160. }
  161. if (isset($subresourceOperation['openapi_context'])) {
  162. $operation['openapi_context'] = $subresourceOperation['openapi_context'];
  163. }
  164. foreach (self::ROUTE_OPTIONS as $routeOption => $defaultValue) {
  165. $operation[$routeOption] = $subresourceOperation[$routeOption] ?? $defaultValue;
  166. }
  167. $tree[$operation['route_name']] = $operation;
  168. // Get the minimum maxDepth between the rootMaxDepth and the maxDepth of the to be visited Subresource
  169. $currentMaxDepth = array_filter([$maxDepth, $subresource->getMaxDepth()], 'is_int');
  170. $currentMaxDepth = empty($currentMaxDepth) ? null : min($currentMaxDepth);
  171. $this->computeSubresourceOperations($subresourceClass, $tree, $rootResourceClass, $operation, $visited + [$visiting => true], $depth + 1, $currentMaxDepth);
  172. }
  173. }
  174. }