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

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