Skip to content

Commit 75b3682

Browse files
authored
Merge pull request #1245 from soyuka/swagger-subresource2
Swagger subresource + OperationPathResolver refactor proposal
2 parents 131c91b + e109a25 commit 75b3682

35 files changed

+1157
-219
lines changed

features/bootstrap/FeatureContext.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,9 +609,11 @@ public function thereIsAnAnswerToTheQuestion(string $a, string $q)
609609
$question = new Question();
610610
$question->setContent($q);
611611
$question->setAnswer($answer);
612+
$answer->addRelatedQuestion($question);
612613

613614
$this->manager->persist($answer);
614615
$this->manager->persist($question);
616+
615617
$this->manager->flush();
616618
}
617619

features/main/subresource.feature

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,39 @@ Feature: Subresource support
1212
And the JSON should be equal to:
1313
"""
1414
{
15-
"@context": "\/contexts\/Answer",
16-
"@id": "\/answers\/1",
17-
"@type": "Answer",
18-
"id": 1,
19-
"content": "42",
20-
"question": "\/questions\/1"
15+
"@context": "/contexts/Answer",
16+
"@id": "/answers/1",
17+
"@type": "Answer",
18+
"id": 1,
19+
"content": "42",
20+
"question": "/questions/1",
21+
"relatedQuestions": [
22+
"/questions/1"
23+
]
24+
}
25+
"""
26+
27+
Scenario: Get subresource one to one relation
28+
When I send a "GET" request to "/questions/1/answer/related_questions"
29+
And print last JSON response
30+
And the response status code should be 200
31+
And the response should be in JSON
32+
And the JSON should be equal to:
33+
"""
34+
{
35+
"@context": "/contexts/Question",
36+
"@id": "/questions/1/answer/related_questions",
37+
"@type": "hydra:Collection",
38+
"hydra:member": [
39+
{
40+
"@id": "/questions/1",
41+
"@type": "Question",
42+
"content": "What's the answer to the Ultimate Question of Life, the Universe and Everything?",
43+
"id": 1,
44+
"answer": "/answers/1"
45+
}
46+
],
47+
"hydra:totalItems": 1
2148
}
2249
"""
2350

src/Bridge/Doctrine/Orm/SubresourceDataProvider.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Core\Bridge\Doctrine\Orm;
1515

16+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\FilterEagerLoadingExtension;
1617
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
1718
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
1819
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface;
@@ -105,7 +106,7 @@ public function getSubresource(string $resourceClass, array $identifiers, array
105106
$qb = $manager->createQueryBuilder();
106107
$alias = $queryNameGenerator->generateJoinAlias($identifier);
107108
$relationType = $classMetadata->getAssociationMapping($previousAssociationProperty)['type'];
108-
$normalizedIdentifiers = $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass);
109+
$normalizedIdentifiers = isset($identifiers[$identifier]) ? $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass) : [];
109110

110111
switch ($relationType) {
111112
//MANY_TO_MANY relations need an explicit join so that the identifier part can be retrieved
@@ -169,6 +170,11 @@ public function getSubresource(string $resourceClass, array $identifiers, array
169170

170171
if (true === $context['collection']) {
171172
foreach ($this->collectionExtensions as $extension) {
173+
// We don't need this anymore because we already made sub queries to ensure correct results
174+
if ($extension instanceof FilterEagerLoadingExtension) {
175+
continue;
176+
}
177+
172178
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName);
173179

174180
if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName)) {

src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ private function getContext(Request $request, Documentation $documentation): arr
117117
$swaggerData['operationId'] = sprintf('%s%sCollection', $collectionOperationName, $swaggerData['shortName']);
118118
} elseif (null !== $itemOperationName = $request->attributes->get('_api_item_operation_name')) {
119119
$swaggerData['operationId'] = sprintf('%s%sItem', $itemOperationName, $swaggerData['shortName']);
120+
} elseif (null !== $subresourceOperationContext = $request->attributes->get('_api_subresource_context')) {
121+
$swaggerData['operationId'] = $subresourceOperationContext['operationId'];
120122
}
121123

122124
list($swaggerData['path'], $swaggerData['method']) = $this->getPathAndMethod($swaggerData);

src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,15 @@ public function getConfigTreeBuilder()
4343
->scalarNode('title')->defaultValue('')->info('The title of the API.')->end()
4444
->scalarNode('description')->defaultValue('')->info('The description of the API.')->end()
4545
->scalarNode('version')->defaultValue('0.0.0')->info('The version of the API.')->end()
46-
->scalarNode('default_operation_path_resolver')->defaultValue('api_platform.operation_path_resolver.underscore')->info('Specify the default operation path resolver to use for generating resources operations path.')->end()
46+
->scalarNode('default_operation_path_resolver')
47+
->beforeNormalization()->always(function ($v) {
48+
if (isset($v['default_operation_path_resolver'])) {
49+
@trigger_error('The use of the `default_operation_path_resolver` has been deprecated in 2.1 and will be removed in 3.0. Use `path_segment_name_generator` instead.', E_USER_DEPRECATED);
50+
}
51+
})->end()
52+
->defaultValue('api_platform.operation_path_resolver.underscore')->info('Specify the default operation path resolver to use for generating resources operations path.')->end()
4753
->scalarNode('name_converter')->defaultNull()->info('Specify a name converter to use.')->end()
54+
->scalarNode('path_segment_name_generator')->defaultValue('api_platform.path_segment_name_generator.underscore')->info('Specify a path name generator to use.')->end()
4855
->scalarNode('api_resources_directory')->defaultValue('Entity')->info('The name of the directory within the bundles that contains the api resources.')->end()
4956
->arrayNode('eager_loading')
5057
->canBeDisabled()

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@
3636
<argument type="service" id="service_container" />
3737
<argument>%api_platform.formats%</argument>
3838
<argument>%api_platform.resource_class_directories%</argument>
39-
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
40-
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
39+
<argument type="service" id="api_platform.subresource_operation_factory" />
4140

4241
<tag name="routing.loader" />
4342
</service>
@@ -91,14 +90,31 @@
9190
<service id="api_platform.operation_path_resolver.router" class="ApiPlatform\Core\Bridge\Symfony\Routing\RouterOperationPathResolver" public="false">
9291
<argument type="service" id="api_platform.router" />
9392
<argument type="service" id="api_platform.operation_path_resolver.custom" />
93+
<argument type="service" id="api_platform.subresource_operation_factory" />
9494
</service>
9595

9696
<service id="api_platform.operation_path_resolver.custom" class="ApiPlatform\Core\PathResolver\CustomOperationPathResolver" public="false">
97+
<!-- should be "api_platform.operation_path_resolver.generator" when the default is removed -->
9798
<argument type="service" id="api_platform.operation_path_resolver.default" />
9899
</service>
99100

100-
<service id="api_platform.operation_path_resolver.underscore" class="ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver" public="false" />
101-
<service id="api_platform.operation_path_resolver.dash" class="ApiPlatform\Core\PathResolver\DashOperationPathResolver" public="false" />
101+
<service id="api_platform.operation_path_resolver.generator" class="ApiPlatform\Core\PathResolver\OperationPathResolver" public="false">
102+
<argument type="service" id="api_platform.path_segment_name_generator" />
103+
</service>
104+
105+
<service id="api_platform.operation_path_resolver.underscore" class="ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver" public="false">
106+
<deprecated>The "%service_id%" service is deprecated since ApiPlatform 2.1 and will be removed in 3.0. Use PathSegmentNameGenerator instead.</deprecated>
107+
</service>
108+
109+
<service id="api_platform.operation_path_resolver.dash" class="ApiPlatform\Core\PathResolver\DashOperationPathResolver" public="false">
110+
<deprecated>The "%service_id%" service is deprecated since ApiPlatform 2.1 and will be removed in 3.0. Use PathSegmentNameGenerator instead.</deprecated>
111+
</service>
112+
113+
<!-- Path name generator -->
114+
115+
<service id="api_platform.path_segment_name_generator" alias="api_platform.path_segment_name_generator.underscore" public="false" />
116+
<service id="api_platform.path_segment_name_generator.underscore" class="ApiPlatform\Core\Operation\UnderscorePathSegmentNameGenerator" public="false" />
117+
<service id="api_platform.path_segment_name_generator.dash" class="ApiPlatform\Core\Operation\DashPathSegmentNameGenerator" public="false" />
102118

103119
<!-- Event listeners -->
104120

@@ -194,6 +210,20 @@
194210
<argument type="service" id="api_platform.property_accessor" />
195211
</service>
196212

213+
<!-- Subresources -->
214+
215+
<service id="api_platform.subresource_operation_factory" class="ApiPlatform\Core\Operation\Factory\SubresourceOperationFactory" public="false">
216+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
217+
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
218+
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
219+
<argument type="service" id="api_platform.path_segment_name_generator" />
220+
</service>
221+
222+
<service id="api_platform.subresource_operation_factory.cached" class="ApiPlatform\Core\Operation\Factory\CachedSubresourceOperationFactory" decorates="api_platform.subresource_operation_factory" decoration-priority="-10" public="false">
223+
<argument type="service" id="api_platform.cache.subresource_operation_factory" />
224+
<argument type="service" id="api_platform.subresource_operation_factory.cached.inner" />
225+
</service>
226+
197227
<!-- Cache -->
198228

199229
<service id="api_platform.cache.route_name_resolver" parent="cache.system" public="false">
@@ -203,6 +233,10 @@
203233
<service id="api_platform.cache.identifiers_extractor" parent="cache.system" public="false">
204234
<tag name="cache.pool" />
205235
</service>
236+
237+
<service id="api_platform.cache.subresource_operation_factory" parent="cache.system" public="false">
238+
<tag name="cache.pool" />
239+
</service>
206240
</services>
207241

208242
</container>

src/Bridge/Symfony/Bundle/Resources/config/swagger.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<argument>%api_platform.oauth.tokenUrl%</argument>
2323
<argument>%api_platform.oauth.authorizationUrl%</argument>
2424
<argument>%api_platform.oauth.scopes%</argument>
25+
<argument type="service" id="api_platform.subresource_operation_factory"></argument>
2526
<tag name="serializer.normalizer" priority="16" />
2627
</service>
2728

src/Bridge/Symfony/Routing/ApiLoader.php

Lines changed: 28 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@
1616
use ApiPlatform\Core\Api\OperationType;
1717
use ApiPlatform\Core\Exception\InvalidResourceException;
1818
use ApiPlatform\Core\Exception\RuntimeException;
19-
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
20-
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2119
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2220
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
21+
use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
2322
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
2423
use Symfony\Component\Config\FileLocator;
2524
use Symfony\Component\Config\Loader\Loader;
@@ -42,19 +41,17 @@ final class ApiLoader extends Loader
4241
*/
4342
const ROUTE_NAME_PREFIX = 'api_';
4443
const DEFAULT_ACTION_PATTERN = 'api_platform.action.';
45-
const SUBRESOURCE_SUFFIX = '_get_subresource';
4644

4745
private $fileLoader;
48-
private $propertyNameCollectionFactory;
49-
private $propertyMetadataFactory;
5046
private $resourceNameCollectionFactory;
5147
private $resourceMetadataFactory;
5248
private $operationPathResolver;
5349
private $container;
5450
private $formats;
5551
private $resourceClassDirectories;
52+
private $subresourceOperationFactory;
5653

57-
public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, array $resourceClassDirectories = [], PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory = null, PropertyMetadataFactoryInterface $propertyMetadataFactory = null)
54+
public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, array $resourceClassDirectories = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null)
5855
{
5956
$this->fileLoader = new XmlFileLoader(new FileLocator($kernel->locateResource('@ApiPlatformBundle/Resources/config/routing')));
6057
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
@@ -63,8 +60,7 @@ public function __construct(KernelInterface $kernel, ResourceNameCollectionFacto
6360
$this->container = $container;
6461
$this->formats = $formats;
6562
$this->resourceClassDirectories = $resourceClassDirectories;
66-
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
67-
$this->propertyMetadataFactory = $propertyMetadataFactory;
63+
$this->subresourceOperationFactory = $subresourceOperationFactory;
6864
}
6965

7066
/**
@@ -99,90 +95,35 @@ public function load($data, $type = null): RouteCollection
9995
}
10096
}
10197

102-
$this->computeSubresourceOperations($routeCollection, $resourceClass);
103-
}
104-
105-
return $routeCollection;
106-
}
107-
108-
/**
109-
* Handles subresource operations recursively and declare their corresponding routes.
110-
*
111-
* @param RouteCollection $routeCollection
112-
* @param string $resourceClass
113-
* @param string $rootResourceClass null on the first iteration, it then keeps track of the origin resource class
114-
* @param array $parentOperation the previous call operation
115-
*/
116-
private function computeSubresourceOperations(RouteCollection $routeCollection, string $resourceClass, string $rootResourceClass = null, array $parentOperation = null, array $visited = [])
117-
{
118-
if (null === $this->propertyNameCollectionFactory || null === $this->propertyMetadataFactory) {
119-
return;
120-
}
121-
122-
if (null === $rootResourceClass) {
123-
$rootResourceClass = $resourceClass;
124-
}
125-
126-
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
127-
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
128-
129-
if (!$propertyMetadata->hasSubresource()) {
130-
continue;
131-
}
132-
133-
$subresource = $propertyMetadata->getSubresource();
134-
135-
$operation = [
136-
'property' => $property,
137-
'collection' => $subresource->isCollection(),
138-
];
139-
140-
$visiting = "$rootResourceClass $resourceClass $property {$subresource->isCollection()} {$subresource->getResourceClass()}";
141-
142-
if (in_array($visiting, $visited, true)) {
98+
if (null === $this->subresourceOperationFactory) {
14399
continue;
144100
}
145101

146-
$visited[] = $visiting;
147-
148-
if (null === $parentOperation) {
149-
$rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass);
150-
$rootShortname = $rootResourceMetadata->getShortName();
151-
152-
$operation['identifiers'] = [['id', $rootResourceClass]];
153-
$operation['route_name'] = RouteNameGenerator::generate('get', $rootShortname, OperationType::SUBRESOURCE, $operation);
154-
$operation['path'] = $this->operationPathResolver->resolveOperationPath($rootShortname, $operation, OperationType::SUBRESOURCE, $operation['route_name']);
155-
} else {
156-
$operation['identifiers'] = $parentOperation['identifiers'];
157-
$operation['identifiers'][] = [$parentOperation['property'], $resourceClass];
158-
$operation['route_name'] = str_replace('get'.RouteNameGenerator::SUBRESOURCE_SUFFIX, RouteNameGenerator::routeNameResolver($property, $operation['collection']).'_get'.RouteNameGenerator::SUBRESOURCE_SUFFIX, $parentOperation['route_name']);
159-
$operation['path'] = $this->operationPathResolver->resolveOperationPath($parentOperation['path'], $operation, OperationType::SUBRESOURCE, $operation['route_name']);
160-
}
161-
162-
$route = new Route(
163-
$operation['path'],
164-
[
165-
'_controller' => self::DEFAULT_ACTION_PATTERN.'get_subresource',
166-
'_format' => null,
167-
'_api_resource_class' => $subresource->getResourceClass(),
168-
'_api_subresource_operation_name' => $operation['route_name'],
169-
'_api_subresource_context' => [
170-
'property' => $operation['property'],
171-
'identifiers' => $operation['identifiers'],
172-
'collection' => $subresource->isCollection(),
102+
foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $operation) {
103+
$routeCollection->add($operation['route_name'], new Route(
104+
$operation['path'],
105+
[
106+
'_controller' => self::DEFAULT_ACTION_PATTERN.'get_subresource',
107+
'_format' => null,
108+
'_api_resource_class' => $operation['resource_class'],
109+
'_api_subresource_operation_name' => $operation['route_name'],
110+
'_api_subresource_context' => [
111+
'property' => $operation['property'],
112+
'identifiers' => $operation['identifiers'],
113+
'collection' => $operation['collection'],
114+
'operationId' => $operationId,
115+
],
173116
],
174-
],
175-
[],
176-
[],
177-
'',
178-
[],
179-
['GET']
180-
);
181-
182-
$routeCollection->add($operation['route_name'], $route);
183-
184-
$this->computeSubresourceOperations($routeCollection, $subresource->getResourceClass(), $rootResourceClass, $operation, $visited);
117+
[],
118+
[],
119+
'',
120+
[],
121+
['GET']
122+
));
123+
}
185124
}
125+
126+
return $routeCollection;
186127
}
187128

188129
/**

0 commit comments

Comments
 (0)