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

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