Skip to content

Commit 5fc0e5e

Browse files
committed
Fix path for custom operation with Swagger UI
1 parent fe5d7dd commit 5fc0e5e

18 files changed

+392
-105
lines changed

src/Api/OperationTypeDeprecationHelper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* Because we introduced a third type in API Platform 2.1, we're using a string with OperationType constants:
2323
* - OperationType::ITEM
2424
* - OperationType::COLLECTION
25-
* - OperationType::SUBCOLLECTION
25+
* - OperationType::SUBRESOURCE
2626
*
2727
* @internal
2828
*/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ private function getContext(Request $request, Documentation $documentation): arr
9292

9393
$swaggerData = [
9494
'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']),
95-
'spec' => $this->normalizer->normalize($documentation, 'json'),
95+
'spec' => $this->normalizer->normalize($documentation, 'json', ['base_url' => $request->getBaseUrl()]),
9696
];
9797

9898
$swaggerData['oauth'] = [

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<argument type="service" id="api_platform.resource_class_resolver" />
1414
<argument type="service" id="api_platform.operation_method_resolver" />
1515
<argument type="service" id="api_platform.operation_path_resolver" />
16-
<argument type="service" id="api_platform.router" />
16+
<argument>null</argument>
1717
<argument type="service" id="api_platform.filter_locator" />
1818
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
1919
<argument>%api_platform.oauth.enabled%</argument>

src/Bridge/Symfony/Routing/ApiLoader.php

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
*/
3939
final class ApiLoader extends Loader
4040
{
41+
/**
42+
* @deprecated since version 2.1, to be removed in 3.0. Use {@see RouteNameGenerator::ROUTE_NAME_PREFIX} instead.
43+
*/
4144
const ROUTE_NAME_PREFIX = 'api_';
4245
const DEFAULT_ACTION_PATTERN = 'api_platform.action.';
4346
const SUBRESOURCE_SUFFIX = '_get_subresource';
@@ -166,13 +169,13 @@ private function computeSubresourceOperations(RouteCollection $routeCollection,
166169
$resourceRouteName = $this->routeNameResolver($rootShortname);
167170

168171
$operation['identifiers'] = [['id', $rootResourceClass]];
169-
$operation['route_name'] = sprintf('%s%s_%s%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $propertyName, self::SUBRESOURCE_SUFFIX);
170-
$operation['path'] = $this->operationPathResolver->resolveOperationPath($rootShortname, $operation, OperationType::SUBRESOURCE);
172+
$operation['route_name'] = sprintf('%s%s_%s%s', RouteNameGenerator::ROUTE_NAME_PREFIX, $resourceRouteName, $propertyName, self::SUBRESOURCE_SUFFIX);
173+
$operation['path'] = $this->operationPathResolver->resolveOperationPath($rootShortname, $operation, OperationType::SUBRESOURCE, $operation['route_name']);
171174
} else {
172175
$operation['identifiers'] = $parentOperation['identifiers'];
173176
$operation['identifiers'][] = [$parentOperation['property'], $resourceClass];
174177
$operation['route_name'] = str_replace(self::SUBRESOURCE_SUFFIX, "_$propertyName".self::SUBRESOURCE_SUFFIX, $parentOperation['route_name']);
175-
$operation['path'] = $this->operationPathResolver->resolveOperationPath($parentOperation['path'], $operation, OperationType::SUBRESOURCE);
178+
$operation['path'] = $this->operationPathResolver->resolveOperationPath($parentOperation['path'], $operation, OperationType::SUBRESOURCE, $operation['route_name']);
176179
}
177180

178181
$route = new Route(
@@ -245,28 +248,16 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
245248
throw new RuntimeException('Either a "route_name" or a "method" operation attribute must exist.');
246249
}
247250

248-
$controller = $operation['controller'] ?? null;
249-
$actionName = sprintf('%s_%s', strtolower($operation['method']), $operationType);
250-
251-
if (null === $controller) {
252-
$controller = self::DEFAULT_ACTION_PATTERN.$actionName;
251+
if (null === $controller = $operation['controller'] ?? null) {
252+
$controller = sprintf('%s%s_%s', self::DEFAULT_ACTION_PATTERN, strtolower($operation['method']), $operationType);
253253

254254
if (!$this->container->has($controller)) {
255255
throw new RuntimeException(sprintf('There is no builtin action for the %s %s operation. You need to define the controller yourself.', $operationType, $operation['method']));
256256
}
257257
}
258258

259-
if ($operationName !== strtolower($operation['method'])) {
260-
$actionName = sprintf('%s_%s', $operationName, $operationType);
261-
}
262-
263-
$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType);
264-
265-
$resourceRouteName = $this->routeNameResolver($resourceShortName);
266-
$routeName = sprintf('%s%s_%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $actionName);
267-
268259
$route = new Route(
269-
$path,
260+
$this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName),
270261
[
271262
'_controller' => $controller,
272263
'_format' => null,
@@ -280,6 +271,6 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
280271
[$operation['method']]
281272
);
282273

283-
$routeCollection->add($routeName, $route);
274+
$routeCollection->add(RouteNameGenerator::generate($operationName, $resourceShortName, $operationType), $route);
284275
}
285276
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Bridge\Symfony\Routing;
15+
16+
use ApiPlatform\Core\Api\OperationType;
17+
use ApiPlatform\Core\Api\OperationTypeDeprecationHelper;
18+
use ApiPlatform\Core\Exception\InvalidArgumentException;
19+
use Doctrine\Common\Util\Inflector;
20+
21+
/**
22+
* Generates the Symfony route name associated with an operation name and a resource short name.
23+
*
24+
* @internal
25+
*
26+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
27+
*/
28+
class RouteNameGenerator
29+
{
30+
const ROUTE_NAME_PREFIX = 'api_';
31+
32+
private function __construct()
33+
{
34+
}
35+
36+
/**
37+
* Generates a Symfony route name.
38+
*
39+
* @param string $operationName
40+
* @param string $resourceShortName
41+
* @param string|bool $operationType
42+
*
43+
* @throws InvalidArgumentException
44+
*
45+
* @return string
46+
*/
47+
public static function generate(string $operationName, string $resourceShortName, $operationType): string
48+
{
49+
if (OperationType::SUBRESOURCE === $operationType = OperationTypeDeprecationHelper::getOperationType($operationType)) {
50+
throw new InvalidArgumentException();
51+
}
52+
53+
return sprintf(
54+
'%s%s_%s_%s',
55+
static::ROUTE_NAME_PREFIX,
56+
Inflector::pluralize(Inflector::tableize($resourceShortName)),
57+
$operationName,
58+
$operationType
59+
);
60+
}
61+
}

src/Bridge/Symfony/Routing/RouterOperationPathResolver.php

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

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

16+
use ApiPlatform\Core\Api\OperationType;
17+
use ApiPlatform\Core\Api\OperationTypeDeprecationHelper;
1618
use ApiPlatform\Core\Exception\InvalidArgumentException;
1719
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
1820
use Symfony\Component\Routing\RouterInterface;
@@ -38,15 +40,38 @@ public function __construct(RouterInterface $router, OperationPathResolverInterf
3840
*
3941
* @throws InvalidArgumentException
4042
*/
41-
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType): string
43+
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
4244
{
43-
if (!isset($operation['route_name'])) {
44-
return $this->deferred->resolveOperationPath($resourceShortName, $operation, $operationType);
45+
if (func_num_args() >= 4) {
46+
$operationName = func_get_arg(3);
47+
} else {
48+
@trigger_error(
49+
sprintf(
50+
'Method %s() will have a 4th `string $operationName` argument in version 3.0. Not defining it is deprecated since 2.1.',
51+
__METHOD__
52+
),
53+
E_USER_DEPRECATED
54+
);
55+
56+
$operationName = null;
57+
}
58+
59+
if (OperationType::SUBRESOURCE === $operationType = OperationTypeDeprecationHelper::getOperationType($operationType)) {
60+
return $this->deferred->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
61+
}
62+
63+
if (isset($operation['route_name'])) {
64+
$routeName = $operation['route_name'];
65+
} elseif (null !== $operationName) {
66+
$routeName = RouteNameGenerator::generate($operationName, $resourceShortName, $operationType);
67+
} else {
68+
return $this->deferred->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
4569
}
4670

47-
$route = $this->router->getRouteCollection()->get($operation['route_name']);
48-
if (null === $route) {
49-
throw new InvalidArgumentException(sprintf('The route "%s" of the resource "%s" was not found.', $operation['route_name'], $resourceShortName));
71+
if (!$route = $this->router->getRouteCollection()->get($routeName)) {
72+
throw new InvalidArgumentException(
73+
sprintf('The route "%s" of the resource "%s" was not found.', $routeName, $resourceShortName)
74+
);
5075
}
5176

5277
return $route->getPath();

src/PathResolver/CustomOperationPathResolver.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,20 @@ public function __construct(OperationPathResolverInterface $deferred)
3232
/**
3333
* {@inheritdoc}
3434
*/
35-
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType): string
35+
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
3636
{
37+
if (func_num_args() >= 4) {
38+
$operationName = func_get_arg(3);
39+
} else {
40+
@trigger_error(sprintf('Method %s() will have a 4th `string $operationName` argument in version 3.0. Not defining it is deprecated since 2.1.', __METHOD__), E_USER_DEPRECATED);
41+
42+
$operationName = null;
43+
}
44+
3745
if (isset($operation['path'])) {
3846
return $operation['path'];
3947
}
4048

41-
return $this->deferred->resolveOperationPath($resourceShortName, $operation, OperationTypeDeprecationHelper::getOperationType($operationType));
49+
return $this->deferred->resolveOperationPath($resourceShortName, $operation, OperationTypeDeprecationHelper::getOperationType($operationType), $operationName);
4250
}
4351
}

src/PathResolver/DashOperationPathResolver.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ final class DashOperationPathResolver implements OperationPathResolverInterface
2727
/**
2828
* {@inheritdoc}
2929
*/
30-
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType): string
30+
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
3131
{
32+
if (func_num_args() < 4) {
33+
@trigger_error(sprintf('Method %s() will have a 4th `string $operationName` argument in version 3.0. Not defining it is deprecated since 2.1.', __METHOD__), E_USER_DEPRECATED);
34+
}
35+
3236
$operationType = OperationTypeDeprecationHelper::getOperationType($operationType);
3337

3438
if ($operationType === OperationType::SUBRESOURCE && 1 < count($operation['identifiers'])) {

src/PathResolver/OperationPathResolverInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ interface OperationPathResolverInterface
2727
* @param array $operation The operation metadata
2828
* @param string|bool $operationType One of the constants defined in ApiPlatform\Core\Api\OperationType
2929
* If the property is a boolean, true represents OperationType::COLLECTION, false is for OperationType::ITEM
30+
* @param string $operationName The operation name
3031
*
3132
* @return string
3233
*/
33-
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType): string;
34+
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string;
3435
}

src/PathResolver/UnderscoreOperationPathResolver.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ final class UnderscoreOperationPathResolver implements OperationPathResolverInte
2727
/**
2828
* {@inheritdoc}
2929
*/
30-
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType): string
30+
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
3131
{
32+
if (func_num_args() < 4) {
33+
@trigger_error(sprintf('Method %s() will have a 4th `string $operationName` argument in version 3.0. Not defining it is deprecated since 2.1.', __METHOD__), E_USER_DEPRECATED);
34+
}
35+
3236
$operationType = OperationTypeDeprecationHelper::getOperationType($operationType);
3337

3438
if ($operationType === OperationType::SUBRESOURCE && 1 < count($operation['identifiers'])) {

src/Swagger/Serializer/DocumentationNormalizer.php

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ final class DocumentationNormalizer implements NormalizerInterface
5151
private $resourceClassResolver;
5252
private $operationMethodResolver;
5353
private $operationPathResolver;
54-
private $urlGenerator;
5554
private $nameConverter;
5655
private $oauthEnabled;
5756
private $oauthType;
@@ -63,8 +62,12 @@ final class DocumentationNormalizer implements NormalizerInterface
6362
/**
6463
* @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection
6564
*/
66-
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator, $filterLocator = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [])
65+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [])
6766
{
67+
if ($urlGenerator) {
68+
@trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.1 and will be removed in 3.0.', UrlGeneratorInterface::class, __METHOD__), E_USER_DEPRECATED);
69+
}
70+
6871
$this->setFilterLocator($filterLocator, true);
6972

7073
$this->resourceMetadataFactory = $resourceMetadataFactory;
@@ -73,7 +76,6 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
7376
$this->resourceClassResolver = $resourceClassResolver;
7477
$this->operationMethodResolver = $operationMethodResolver;
7578
$this->operationPathResolver = $operationPathResolver;
76-
$this->urlGenerator = $urlGenerator;
7779
$this->nameConverter = $nameConverter;
7880
$this->oauthEnabled = $oauthEnabled;
7981
$this->oauthType = $oauthType;
@@ -103,7 +105,7 @@ public function normalize($object, $format = null, array $context = [])
103105
$definitions->ksort();
104106
$paths->ksort();
105107

106-
return $this->computeDoc($object, $definitions, $paths);
108+
return $this->computeDoc($object, $definitions, $paths, $context);
107109
}
108110

109111
/**
@@ -124,7 +126,7 @@ private function addPaths(\ArrayObject $paths, \ArrayObject $definitions, string
124126
}
125127

126128
foreach ($operations as $operationName => $operation) {
127-
$path = $this->getPath($resourceShortName, $operation, $operationType);
129+
$path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);
128130
$method = $operationType === OperationType::ITEM ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
129131

130132
$paths[$path][strtolower($method)] = $this->getPathOperation($operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions);
@@ -140,14 +142,15 @@ private function addPaths(\ArrayObject $paths, \ArrayObject $definitions, string
140142
* @see https://github.com/OAI/OpenAPI-Specification/issues/93
141143
*
142144
* @param string $resourceShortName
145+
* @param string $operationName
143146
* @param array $operation
144147
* @param string $operationType
145148
*
146149
* @return string
147150
*/
148-
private function getPath(string $resourceShortName, array $operation, string $operationType): string
151+
private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string
149152
{
150-
$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType);
153+
$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
151154
if ('.{_format}' === substr($path, -10)) {
152155
$path = substr($path, 0, -10);
153156
}
@@ -211,7 +214,7 @@ private function updateGetOperation(\ArrayObject $pathOperation, array $mimeType
211214

212215
$pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
213216

214-
if ($operationType === OperationType::COLLECTION || $operationType === OperationType::SUBRESOURCE) {
217+
if ($operationType === OperationType::COLLECTION) {
215218
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName);
216219
$pathOperation['responses'] ?? $pathOperation['responses'] = [
217220
'200' => [
@@ -526,14 +529,15 @@ private function getType(string $type, bool $isCollection, string $className = n
526529
* @param Documentation $documentation
527530
* @param \ArrayObject $definitions
528531
* @param \ArrayObject $paths
532+
* @param array $context
529533
*
530534
* @return array
531535
*/
532-
private function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths): array
536+
private function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
533537
{
534538
$doc = [
535539
'swagger' => self::SWAGGER_VERSION,
536-
'basePath' => $this->urlGenerator->generate('api_entrypoint'),
540+
'basePath' => $context['base_url'] ?? '/',
537541
'info' => [
538542
'title' => $documentation->getTitle(),
539543
'version' => $documentation->getVersion(),

0 commit comments

Comments
 (0)