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), |
||
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 |