1 | <?php |
||||
2 | |||||
3 | /* |
||||
4 | * This file is part of the API Platform project. |
||||
5 | * |
||||
6 | * (c) Kévin Dunglas <[email protected]> |
||||
7 | * |
||||
8 | * For the full copyright and license information, please view the LICENSE |
||||
9 | * file that was distributed with this source code. |
||||
10 | */ |
||||
11 | |||||
12 | declare(strict_types=1); |
||||
13 | |||||
14 | namespace ApiPlatform\Core\Operation\Factory; |
||||
15 | |||||
16 | use ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameGenerator; |
||||
17 | use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
||||
18 | use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
||||
19 | use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
||||
20 | use ApiPlatform\Core\Operation\PathSegmentNameGeneratorInterface; |
||||
21 | |||||
22 | /** |
||||
23 | * @internal |
||||
24 | */ |
||||
25 | final class SubresourceOperationFactory implements SubresourceOperationFactoryInterface |
||||
26 | { |
||||
27 | public const SUBRESOURCE_SUFFIX = '_subresource'; |
||||
28 | public const FORMAT_SUFFIX = '.{_format}'; |
||||
29 | public const ROUTE_OPTIONS = ['defaults' => [], 'requirements' => [], 'options' => [], 'host' => '', 'schemes' => [], 'condition' => '', 'controller' => null]; |
||||
30 | |||||
31 | private $resourceMetadataFactory; |
||||
32 | private $propertyNameCollectionFactory; |
||||
33 | private $propertyMetadataFactory; |
||||
34 | private $pathSegmentNameGenerator; |
||||
35 | |||||
36 | public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PathSegmentNameGeneratorInterface $pathSegmentNameGenerator) |
||||
37 | { |
||||
38 | $this->resourceMetadataFactory = $resourceMetadataFactory; |
||||
39 | $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
||||
40 | $this->propertyMetadataFactory = $propertyMetadataFactory; |
||||
41 | $this->pathSegmentNameGenerator = $pathSegmentNameGenerator; |
||||
42 | } |
||||
43 | |||||
44 | /** |
||||
45 | * {@inheritdoc} |
||||
46 | */ |
||||
47 | public function create(string $resourceClass): array |
||||
48 | { |
||||
49 | $tree = []; |
||||
50 | $this->computeSubresourceOperations($resourceClass, $tree); |
||||
51 | |||||
52 | return $tree; |
||||
53 | } |
||||
54 | |||||
55 | /** |
||||
56 | * Handles subresource operations recursively and declare their corresponding routes. |
||||
57 | * |
||||
58 | * @param string $rootResourceClass null on the first iteration, it then keeps track of the origin resource class |
||||
59 | * @param array $parentOperation the previous call operation |
||||
60 | * @param int $depth the number of visited |
||||
61 | * @param int $maxDepth |
||||
62 | */ |
||||
63 | private function computeSubresourceOperations(string $resourceClass, array &$tree, string $rootResourceClass = null, array $parentOperation = null, array $visited = [], int $depth = 0, int $maxDepth = null): void |
||||
64 | { |
||||
65 | if (null === $rootResourceClass) { |
||||
66 | $rootResourceClass = $resourceClass; |
||||
67 | } |
||||
68 | |||||
69 | foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) { |
||||
70 | $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property); |
||||
71 | |||||
72 | if (!$subresource = $propertyMetadata->getSubresource()) { |
||||
73 | continue; |
||||
74 | } |
||||
75 | |||||
76 | $subresourceClass = $subresource->getResourceClass(); |
||||
77 | $subresourceMetadata = $this->resourceMetadataFactory->create($subresourceClass); |
||||
78 | $isLastItem = $resourceClass === $parentOperation['resource_class'] && $propertyMetadata->isIdentifier(); |
||||
79 | |||||
80 | // A subresource that is also an identifier can't be a start point |
||||
81 | if ($isLastItem && (null === $parentOperation || false === $parentOperation['collection'])) { |
||||
82 | continue; |
||||
83 | } |
||||
84 | |||||
85 | $visiting = "$resourceClass $property $subresourceClass"; |
||||
86 | |||||
87 | // Handle maxDepth |
||||
88 | if ($rootResourceClass === $resourceClass) { |
||||
89 | $maxDepth = $subresource->getMaxDepth(); |
||||
90 | // reset depth when we return to rootResourceClass |
||||
91 | $depth = 0; |
||||
92 | } |
||||
93 | |||||
94 | if (null !== $maxDepth && $depth >= $maxDepth) { |
||||
95 | break; |
||||
96 | } |
||||
97 | if (isset($visited[$visiting])) { |
||||
98 | continue; |
||||
99 | } |
||||
100 | |||||
101 | $rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass); |
||||
102 | $operationName = 'get'; |
||||
103 | $operation = [ |
||||
104 | 'property' => $property, |
||||
105 | 'collection' => $subresource->isCollection(), |
||||
106 | 'resource_class' => $subresourceClass, |
||||
107 | 'shortNames' => [$subresourceMetadata->getShortName()], |
||||
108 | ]; |
||||
109 | |||||
110 | if (null === $parentOperation) { |
||||
111 | $rootShortname = $rootResourceMetadata->getShortName(); |
||||
112 | $operation['identifiers'] = [['id', $rootResourceClass, true]]; |
||||
113 | $operation['operation_name'] = sprintf( |
||||
114 | '%s_%s%s', |
||||
115 | RouteNameGenerator::inflector($operation['property'], $operation['collection'] ?? false), |
||||
116 | $operationName, |
||||
117 | self::SUBRESOURCE_SUFFIX |
||||
118 | ); |
||||
119 | |||||
120 | $subresourceOperation = $rootResourceMetadata->getSubresourceOperations()[$operation['operation_name']] ?? []; |
||||
121 | |||||
122 | $operation['route_name'] = sprintf( |
||||
123 | '%s%s_%s', |
||||
124 | RouteNameGenerator::ROUTE_NAME_PREFIX, |
||||
125 | RouteNameGenerator::inflector($rootShortname), |
||||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
126 | $operation['operation_name'] |
||||
127 | ); |
||||
128 | |||||
129 | $prefix = trim(trim($rootResourceMetadata->getAttribute('route_prefix', '')), '/'); |
||||
130 | if ('' !== $prefix) { |
||||
131 | $prefix .= '/'; |
||||
132 | } |
||||
133 | |||||
134 | $operation['path'] = $subresourceOperation['path'] ?? sprintf( |
||||
135 | '/%s%s/{id}/%s%s', |
||||
136 | $prefix, |
||||
137 | $this->pathSegmentNameGenerator->getSegmentName($rootShortname), |
||||
0 ignored issues
–
show
It seems like
$rootShortname can also be of type null ; however, parameter $name of ApiPlatform\Core\Operati...rface::getSegmentName() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
138 | $this->pathSegmentNameGenerator->getSegmentName($operation['property'], $operation['collection']), |
||||
139 | self::FORMAT_SUFFIX |
||||
140 | ); |
||||
141 | |||||
142 | if (!\in_array($rootShortname, $operation['shortNames'], true)) { |
||||
143 | $operation['shortNames'][] = $rootShortname; |
||||
144 | } |
||||
145 | } else { |
||||
146 | $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
||||
147 | $operation['identifiers'] = $parentOperation['identifiers']; |
||||
148 | $operation['identifiers'][] = [$parentOperation['property'], $resourceClass, $isLastItem ? true : $parentOperation['collection']]; |
||||
149 | $operation['operation_name'] = str_replace( |
||||
150 | 'get'.self::SUBRESOURCE_SUFFIX, |
||||
151 | RouteNameGenerator::inflector($isLastItem ? 'item' : $property, $operation['collection']).'_get'.self::SUBRESOURCE_SUFFIX, |
||||
152 | $parentOperation['operation_name'] |
||||
153 | ); |
||||
154 | $operation['route_name'] = str_replace($parentOperation['operation_name'], $operation['operation_name'], $parentOperation['route_name']); |
||||
155 | |||||
156 | if (!\in_array($resourceMetadata->getShortName(), $operation['shortNames'], true)) { |
||||
157 | $operation['shortNames'][] = $resourceMetadata->getShortName(); |
||||
158 | } |
||||
159 | |||||
160 | $subresourceOperation = $rootResourceMetadata->getSubresourceOperations()[$operation['operation_name']] ?? []; |
||||
161 | |||||
162 | if (isset($subresourceOperation['path'])) { |
||||
163 | $operation['path'] = $subresourceOperation['path']; |
||||
164 | } else { |
||||
165 | $operation['path'] = str_replace(self::FORMAT_SUFFIX, '', (string) $parentOperation['path']); |
||||
166 | |||||
167 | if ($parentOperation['collection']) { |
||||
168 | [$key] = end($operation['identifiers']); |
||||
169 | $operation['path'] .= sprintf('/{%s}', $key); |
||||
170 | } |
||||
171 | |||||
172 | if ($isLastItem) { |
||||
173 | $operation['path'] .= self::FORMAT_SUFFIX; |
||||
174 | } else { |
||||
175 | $operation['path'] .= sprintf('/%s%s', $this->pathSegmentNameGenerator->getSegmentName($property, $operation['collection']), self::FORMAT_SUFFIX); |
||||
176 | } |
||||
177 | } |
||||
178 | } |
||||
179 | |||||
180 | foreach (self::ROUTE_OPTIONS as $routeOption => $defaultValue) { |
||||
181 | $operation[$routeOption] = $subresourceOperation[$routeOption] ?? $defaultValue; |
||||
182 | } |
||||
183 | |||||
184 | $tree[$operation['route_name']] = $operation; |
||||
185 | |||||
186 | $this->computeSubresourceOperations($subresourceClass, $tree, $rootResourceClass, $operation, $visited + [$visiting => true], ++$depth, $maxDepth); |
||||
187 | } |
||||
188 | } |
||||
189 | } |
||||
190 |