Skip to content

Commit 7b9c5de

Browse files
soyukaabluchet
authored andcommitted
Refactor path resolver, allow subresources in swagger
1. Implement SubresourceOperationFactory 2. Deprecates UnderscoreOperationPathResolver/DashOperationPathResolver in favor of PathSegmentNameGenerator
1 parent 671b446 commit 7b9c5de

28 files changed

+1056
-200
lines changed

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: 27 additions & 85 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,7 +41,6 @@ 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;
4846
private $propertyNameCollectionFactory;
@@ -53,8 +51,9 @@ final class ApiLoader extends Loader
5351
private $container;
5452
private $formats;
5553
private $resourceClassDirectories;
54+
private $subresourceOperationFactory;
5655

57-
public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, array $resourceClassDirectories = [], PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory = null, PropertyMetadataFactoryInterface $propertyMetadataFactory = null)
56+
public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, array $resourceClassDirectories = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null)
5857
{
5958
$this->fileLoader = new XmlFileLoader(new FileLocator($kernel->locateResource('@ApiPlatformBundle/Resources/config/routing')));
6059
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
@@ -63,8 +62,7 @@ public function __construct(KernelInterface $kernel, ResourceNameCollectionFacto
6362
$this->container = $container;
6463
$this->formats = $formats;
6564
$this->resourceClassDirectories = $resourceClassDirectories;
66-
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
67-
$this->propertyMetadataFactory = $propertyMetadataFactory;
65+
$this->subresourceOperationFactory = $subresourceOperationFactory;
6866
}
6967

7068
/**
@@ -99,90 +97,34 @@ public function load($data, $type = null): RouteCollection
9997
}
10098
}
10199

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)) {
100+
if (null === $this->subresourceOperationFactory) {
143101
continue;
144102
}
145103

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(),
104+
foreach ($this->subresourceOperationFactory->create($resourceClass) as $operation) {
105+
$routeCollection->add($operation['route_name'], new Route(
106+
$operation['path'],
107+
[
108+
'_controller' => self::DEFAULT_ACTION_PATTERN.'get_subresource',
109+
'_format' => null,
110+
'_api_resource_class' => $operation['resource_class'],
111+
'_api_subresource_operation_name' => $operation['route_name'],
112+
'_api_subresource_context' => [
113+
'property' => $operation['property'],
114+
'identifiers' => $operation['identifiers'],
115+
'collection' => $operation['collection'],
116+
],
173117
],
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);
118+
[],
119+
[],
120+
'',
121+
[],
122+
['GET']
123+
));
124+
}
185125
}
126+
127+
return $routeCollection;
186128
}
187129

188130
/**

src/Bridge/Symfony/Routing/RouteNameGenerator.php

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,9 @@
2525
*
2626
* @author Baptiste Meyer <[email protected]>
2727
*/
28-
class RouteNameGenerator
28+
final class RouteNameGenerator
2929
{
3030
const ROUTE_NAME_PREFIX = 'api_';
31-
const SUBRESOURCE_SUFFIX = '_subresource';
3231

3332
private function __construct()
3433
{
@@ -40,33 +39,21 @@ private function __construct()
4039
* @param string $operationName
4140
* @param string $resourceShortName
4241
* @param string|bool $operationType
43-
* @param array $subresourceContext
4442
*
4543
* @throws InvalidArgumentException
4644
*
4745
* @return string
4846
*/
49-
public static function generate(string $operationName, string $resourceShortName, $operationType, array $subresourceContext = []): string
47+
public static function generate(string $operationName, string $resourceShortName, $operationType): string
5048
{
5149
if (OperationType::SUBRESOURCE === $operationType = OperationTypeDeprecationHelper::getOperationType($operationType)) {
52-
if (!isset($subresourceContext['property'])) {
53-
throw new InvalidArgumentException('Missing "property" to generate a route name from a subresource');
54-
}
55-
56-
return sprintf(
57-
'%s%s_%s_%s%s',
58-
static::ROUTE_NAME_PREFIX,
59-
self::routeNameResolver($resourceShortName),
60-
self::routeNameResolver($subresourceContext['property'], $subresourceContext['collection'] ?? false),
61-
$operationName,
62-
self::SUBRESOURCE_SUFFIX
63-
);
50+
throw new InvalidArgumentException('Subresource operations are not supported by the RouteNameGenerator.');
6451
}
6552

6653
return sprintf(
6754
'%s%s_%s_%s',
6855
static::ROUTE_NAME_PREFIX,
69-
self::routeNameResolver($resourceShortName),
56+
self::inflector($resourceShortName),
7057
$operationName,
7158
$operationType
7259
);
@@ -79,7 +66,7 @@ public static function generate(string $operationName, string $resourceShortName
7966
*
8067
* @return string A string that is a part of the route name
8168
*/
82-
public static function routeNameResolver(string $name, bool $pluralize = true): string
69+
public static function inflector(string $name, bool $pluralize = true): string
8370
{
8471
$name = Inflector::tableize($name);
8572

src/Bridge/Symfony/Routing/RouterOperationPathResolver.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Core\Bridge\Symfony\Routing;
1515

16+
use ApiPlatform\Core\Api\OperationType;
1617
use ApiPlatform\Core\Api\OperationTypeDeprecationHelper;
1718
use ApiPlatform\Core\Exception\InvalidArgumentException;
1819
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
@@ -51,10 +52,14 @@ public function resolveOperationPath(string $resourceShortName, array $operation
5152

5253
if (isset($operation['route_name'])) {
5354
$routeName = $operation['route_name'];
54-
} elseif (null !== $operationName) {
55-
$routeName = RouteNameGenerator::generate($operationName, $resourceShortName, $operationType, $operation);
55+
} elseif (OperationType::SUBRESOURCE === $operationType) {
56+
throw new InvalidArgumentException('Subresource operations are not supported by the RouterOperationPathResolver.');
5657
} else {
57-
return $this->deferred->resolveOperationPath($resourceShortName, $operation, OperationTypeDeprecationHelper::getOperationType($operationType), $operationName);
58+
if (null !== $operationName) {
59+
$routeName = RouteNameGenerator::generate($operationName, $resourceShortName, $operationType);
60+
} else {
61+
return $this->deferred->resolveOperationPath($resourceShortName, $operation, OperationTypeDeprecationHelper::getOperationType($operationType), $operationName);
62+
}
5863
}
5964

6065
if (!$route = $this->router->getRouteCollection()->get($routeName)) {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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;
15+
16+
use Doctrine\Common\Util\Inflector;
17+
18+
/**
19+
* Generate a path name with an underscore separator according to a string and whether it needs pluralization.
20+
*
21+
* @author Antoine Bluchet <[email protected]>
22+
*/
23+
final class DashPathSegmentNameGenerator implements PathSegmentNameGeneratorInterface
24+
{
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
public function getSegmentName(string $name, bool $pluralize = true): string
29+
{
30+
$name = $this->dashize($name);
31+
32+
return $pluralize ? Inflector::pluralize($name) : $name;
33+
}
34+
35+
private function dashize(string $string): string
36+
{
37+
return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '-$1', $string));
38+
}
39+
}

0 commit comments

Comments
 (0)