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 $resourceMetadataFactoryPropertyNameCollectionFactoryInterface $propertyNameCollectionFactoryPropertyMetadataFactoryInterface $propertyMetadataFactoryPathSegmentNameGeneratorInterface $pathSegmentNameGeneratorIdentifiersExtractorInterface $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 &$treestring $rootResourceClass null, array $parentOperation null, array $visited = [], int $depth 0int $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] ?? $identifiertrue];
  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. }