Skip to content

Commit 537a3b2

Browse files
committed
Fix path for custom operation with Swagger UI
1 parent 7305991 commit 537a3b2

14 files changed

+180
-96
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public function __invoke(Request $request)
5454

5555
return new Response($this->twig->render(
5656
'@ApiPlatform/SwaggerUi/index.html.twig',
57-
$this->getContext($request) + ['spec' => $this->serializer->serialize($documentation, 'json')])
57+
$this->getContext($request) + ['spec' => $this->serializer->serialize($documentation, 'json', ['base_url' => $request->getBaseUrl()])])
5858
);
5959
}
6060

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.filters" />
1818
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
1919

src/Bridge/Symfony/Routing/ApiLoader.php

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1919
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
2020
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
21-
use Doctrine\Common\Inflector\Inflector;
2221
use Symfony\Component\Config\FileLocator;
2322
use Symfony\Component\Config\Loader\Loader;
2423
use Symfony\Component\Config\Resource\DirectoryResource;
@@ -35,6 +34,10 @@
3534
*/
3635
final class ApiLoader extends Loader
3736
{
37+
/**
38+
* @deprecated since version 2.0.10, to be removed in 3.0.
39+
* Use {@see RouteNameGenerator::ROUTE_NAME_PREFIX} instead.
40+
*/
3841
const ROUTE_NAME_PREFIX = 'api_';
3942
const DEFAULT_ACTION_PATTERN = 'api_platform.action.';
4043

@@ -137,29 +140,16 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
137140
throw new RuntimeException('Either a "route_name" or a "method" operation attribute must exist.');
138141
}
139142

140-
$controller = $operation['controller'] ?? null;
141-
$collectionType = $collection ? 'collection' : 'item';
142-
$actionName = sprintf('%s_%s', strtolower($operation['method']), $collectionType);
143-
144-
if (null === $controller) {
145-
$controller = self::DEFAULT_ACTION_PATTERN.$actionName;
143+
if (null === $controller = $operation['controller'] ?? null) {
144+
$controller = sprintf('%s%s_%s', self::DEFAULT_ACTION_PATTERN, strtolower($operation['method']), $collection ? 'collection' : 'item');
146145

147146
if (!$this->container->has($controller)) {
148-
throw new RuntimeException(sprintf('There is no builtin action for the %s %s operation. You need to define the controller yourself.', $collectionType, $operation['method']));
147+
throw new RuntimeException(sprintf('There is no builtin action for the %s %s operation. You need to define the controller yourself.', $collection ? 'collection' : 'item', $operation['method']));
149148
}
150149
}
151150

152-
if ($operationName !== strtolower($operation['method'])) {
153-
$actionName = sprintf('%s_%s', $operationName, $collection ? 'collection' : 'item');
154-
}
155-
156-
$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $collection);
157-
158-
$resourceRouteName = Inflector::pluralize(Inflector::tableize($resourceShortName));
159-
$routeName = sprintf('%s%s_%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $actionName);
160-
161151
$route = new Route(
162-
$path,
152+
$this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $collection, $operationName),
163153
[
164154
'_controller' => $controller,
165155
'_format' => null,
@@ -173,6 +163,6 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
173163
[$operation['method']]
174164
);
175165

176-
$routeCollection->add($routeName, $route);
166+
$routeCollection->add(RouteNameGenerator::generate($operationName, $resourceShortName, $collection), $route);
177167
}
178168
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 Doctrine\Common\Util\Inflector;
17+
18+
/**
19+
* Generates the Symfony route name associated with an operation name and a resource short name.
20+
*
21+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
22+
*/
23+
class RouteNameGenerator
24+
{
25+
const ROUTE_NAME_PREFIX = 'api_';
26+
27+
private function __construct()
28+
{
29+
}
30+
31+
/**
32+
* Generates a Symfony route name.
33+
*
34+
* @param string $operationName
35+
* @param string $resourceShortName
36+
* @param bool $collection
37+
*
38+
* @return string
39+
*/
40+
public static function generate(string $operationName, string $resourceShortName, bool $collection): string
41+
{
42+
return sprintf(
43+
'%s%s_%s_%s',
44+
static::ROUTE_NAME_PREFIX,
45+
Inflector::pluralize(Inflector::tableize($resourceShortName)),
46+
$operationName,
47+
$collection ? 'collection' : 'item'
48+
);
49+
}
50+
}

src/Bridge/Symfony/Routing/RouterOperationPathResolver.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,25 @@ public function __construct(RouterInterface $router, OperationPathResolverInterf
3838
*
3939
* @throws InvalidArgumentException
4040
*/
41-
public function resolveOperationPath(string $resourceShortName, array $operation, bool $collection): string
41+
public function resolveOperationPath(string $resourceShortName, array $operation, bool $collection/*, string $operationName = null*/): string
4242
{
43-
if (!isset($operation['route_name'])) {
44-
return $this->deferred->resolveOperationPath($resourceShortName, $operation, $collection);
43+
if (func_num_args() >= 4) {
44+
$operationName = func_get_arg(3);
45+
} else {
46+
@trigger_error(sprintf('Method %s() will have a 4th `string $operationName = null` argument in version 3.0. Not defining it is deprecated since 2.0.10.', __METHOD__), E_USER_DEPRECATED);
47+
48+
$operationName = null;
49+
}
50+
51+
if (isset($operation['route_name'])) {
52+
$routeName = $operation['route_name'];
53+
} elseif (null !== $operationName) {
54+
$routeName = RouteNameGenerator::generate($operationName, $resourceShortName, $collection);
55+
} else {
56+
return $this->deferred->resolveOperationPath($resourceShortName, $operation, $collection, $operationName);
4557
}
4658

47-
$route = $this->router->getRouteCollection()->get($operation['route_name']);
48-
if (null === $route) {
59+
if (!$route = $this->router->getRouteCollection()->get($routeName)) {
4960
throw new InvalidArgumentException(sprintf('The route "%s" of the resource "%s" was not found.', $operation['route_name'], $resourceShortName));
5061
}
5162

src/PathResolver/CustomOperationPathResolver.php

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

39-
return $this->deferred->resolveOperationPath($resourceShortName, $operation, $collection);
47+
return $this->deferred->resolveOperationPath($resourceShortName, $operation, $collection, $operationName);
4048
}
4149
}

src/PathResolver/DashOperationPathResolver.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ final class DashOperationPathResolver implements OperationPathResolverInterface
2525
/**
2626
* {@inheritdoc}
2727
*/
28-
public function resolveOperationPath(string $resourceShortName, array $operation, bool $collection): string
28+
public function resolveOperationPath(string $resourceShortName, array $operation, bool $collection/*, string $operationName = null*/): string
2929
{
30+
if (func_num_args() < 4) {
31+
@trigger_error(sprintf('Method %s() will have a 4th `string $operationName = null` argument in version 3.0. Not defining it is deprecated since 2.0.10.', __METHOD__), E_USER_DEPRECATED);
32+
}
33+
3034
$path = '/'.Inflector::pluralize(strtolower(preg_replace('~(?<=\\w)([A-Z])~', '-$1', $resourceShortName)));
3135

3236
if (!$collection) {

src/PathResolver/OperationPathResolverInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ interface OperationPathResolverInterface
2626
* @param string $resourceShortName
2727
* @param array $operation The operation metadata
2828
* @param bool $collection
29+
* @param string $operationName
2930
*
3031
* @return string
3132
*/
32-
public function resolveOperationPath(string $resourceShortName, array $operation, bool $collection): string;
33+
public function resolveOperationPath(string $resourceShortName, array $operation, bool $collection/*, string $operationName = null*/): string;
3334
}

src/PathResolver/UnderscoreOperationPathResolver.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ final class UnderscoreOperationPathResolver implements OperationPathResolverInte
2525
/**
2626
* {@inheritdoc}
2727
*/
28-
public function resolveOperationPath(string $resourceShortName, array $operation, bool $collection): string
28+
public function resolveOperationPath(string $resourceShortName, array $operation, bool $collection/*, string $operationName = null*/): string
2929
{
30+
if (func_num_args() < 4) {
31+
@trigger_error(sprintf('Method %s() will have a 4th `string $operationName = null` argument in version 3.0. Not defining it is deprecated since 2.0.10.', __METHOD__), E_USER_DEPRECATED);
32+
}
33+
3034
$path = '/'.Inflector::pluralize(Inflector::tableize($resourceShortName));
3135

3236
if (!$collection) {

src/Swagger/Serializer/DocumentationNormalizer.php

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,21 @@ final class DocumentationNormalizer implements NormalizerInterface
4646
private $resourceClassResolver;
4747
private $operationMethodResolver;
4848
private $operationPathResolver;
49-
private $urlGenerator;
5049
private $filterCollection;
5150
private $nameConverter;
5251

53-
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator, FilterCollection $filterCollection = null, NameConverterInterface $nameConverter = null)
52+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, FilterCollection $filterCollection = null, NameConverterInterface $nameConverter = null)
5453
{
54+
if ($urlGenerator) {
55+
@trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.0.10 and will be removed in 3.0.', UrlGeneratorInterface::class, __METHOD__), E_USER_DEPRECATED);
56+
}
57+
5558
$this->resourceMetadataFactory = $resourceMetadataFactory;
5659
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
5760
$this->propertyMetadataFactory = $propertyMetadataFactory;
5861
$this->resourceClassResolver = $resourceClassResolver;
5962
$this->operationMethodResolver = $operationMethodResolver;
6063
$this->operationPathResolver = $operationPathResolver;
61-
$this->urlGenerator = $urlGenerator;
6264
$this->filterCollection = $filterCollection;
6365
$this->nameConverter = $nameConverter;
6466
}
@@ -83,7 +85,7 @@ public function normalize($object, $format = null, array $context = [])
8385
$definitions->ksort();
8486
$paths->ksort();
8587

86-
return $this->computeDoc($object, $definitions, $paths);
88+
return $this->computeDoc($object, $definitions, $paths, $context);
8789
}
8890

8991
/**
@@ -106,7 +108,7 @@ private function addPaths(\ArrayObject $paths, \ArrayObject $definitions, string
106108
}
107109

108110
foreach ($operations as $operationName => $operation) {
109-
$path = $this->getPath($resourceShortName, $operation, $collection);
111+
$path = $this->getPath($resourceShortName, $operationName, $operation, $collection);
110112
$method = $collection ? $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName);
111113

112114
$paths[$path][strtolower($method)] = $this->getPathOperation($operationName, $operation, $method, $collection, $resourceClass, $resourceMetadata, $mimeTypes, $definitions);
@@ -122,14 +124,15 @@ private function addPaths(\ArrayObject $paths, \ArrayObject $definitions, string
122124
* @see https://github.com/OAI/OpenAPI-Specification/issues/93
123125
*
124126
* @param string $resourceShortName
127+
* @param string $operationName
125128
* @param array $operation
126129
* @param bool $collection
127130
*
128131
* @return string
129132
*/
130-
private function getPath(string $resourceShortName, array $operation, bool $collection): string
133+
private function getPath(string $resourceShortName, string $operationName, array $operation, bool $collection): string
131134
{
132-
$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $collection);
135+
$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $collection, $operationName);
133136
if ('.{_format}' === substr($path, -10)) {
134137
$path = substr($path, 0, -10);
135138
}
@@ -508,14 +511,15 @@ private function getType(string $type, bool $isCollection, string $className = n
508511
* @param Documentation $documentation
509512
* @param \ArrayObject $definitions
510513
* @param \ArrayObject $paths
514+
* @param array $context
511515
*
512516
* @return array
513517
*/
514-
private function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths): array
518+
private function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
515519
{
516520
$doc = [
517521
'swagger' => self::SWAGGER_VERSION,
518-
'basePath' => $this->urlGenerator->generate('api_entrypoint'),
522+
'basePath' => $context['base_url'] ?? '/',
519523
'info' => [
520524
'title' => $documentation->getTitle(),
521525
'version' => $documentation->getVersion(),

tests/Bridge/Symfony/Bundle/Action/SwaggerUiActionTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function testInvoke(Request $request, ProphecyInterface $twigProphecy)
4141
$resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata('F'))->shouldBeCalled();
4242

4343
$serializerProphecy = $this->prophesize(SerializerInterface::class);
44-
$serializerProphecy->serialize(Argument::type(Documentation::class), 'json')->willReturn('hello')->shouldBeCalled();
44+
$serializerProphecy->serialize(Argument::type(Documentation::class), 'json', Argument::type('array'))->willReturn('hello')->shouldBeCalled();
4545

4646
$action = new SwaggerUiAction(
4747
$resourceNameCollectionFactoryProphecy->reveal(),
@@ -93,7 +93,7 @@ public function testDoNotRunCurrentRequest(Request $request)
9393

9494
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
9595
$serializerProphecy = $this->prophesize(SerializerInterface::class);
96-
$serializerProphecy->serialize(Argument::type(Documentation::class), 'json')->willReturn('hello')->shouldBeCalled();
96+
$serializerProphecy->serialize(Argument::type(Documentation::class), 'json', Argument::type('array'))->willReturn('hello')->shouldBeCalled();
9797

9898
$twigProphecy = $this->prophesize(\Twig_Environment::class);
9999
$twigProphecy->render('@ApiPlatform/SwaggerUi/index.html.twig', [

tests/PathResolver/DashOperationPathResolverTest.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,33 @@
1515

1616
use ApiPlatform\Core\PathResolver\DashOperationPathResolver;
1717

18+
/**
19+
* @author Guilhem N. <egetick@gmail.com>
20+
*/
1821
class DashOperationPathResolverTest extends \PHPUnit_Framework_TestCase
1922
{
2023
public function testResolveCollectionOperationPath()
2124
{
2225
$dashOperationPathResolver = new DashOperationPathResolver();
2326

24-
$this->assertSame('/short-names.{_format}', $dashOperationPathResolver->resolveOperationPath('ShortName', [], true));
27+
$this->assertSame('/short-names.{_format}', $dashOperationPathResolver->resolveOperationPath('ShortName', [], true, 'get'));
2528
}
2629

2730
public function testResolveItemOperationPath()
2831
{
2932
$dashOperationPathResolver = new DashOperationPathResolver();
3033

31-
$this->assertSame('/short-names/{id}.{_format}', $dashOperationPathResolver->resolveOperationPath('ShortName', [], false));
34+
$this->assertSame('/short-names/{id}.{_format}', $dashOperationPathResolver->resolveOperationPath('ShortName', [], false, 'get'));
35+
}
36+
37+
/**
38+
* @group legacy
39+
* @expectedDeprecation Method ApiPlatform\Core\PathResolver\DashOperationPathResolver::resolveOperationPath() will have a 4th `string $operationName = null` argument in version 3.0. Not defining it is deprecated since 2.0.10.
40+
*/
41+
public function testLegacyResolveOperationPath()
42+
{
43+
$dashOperationPathResolver = new DashOperationPathResolver();
44+
45+
$this->assertSame('/short-names.{_format}', $dashOperationPathResolver->resolveOperationPath('ShortName', [], true));
3246
}
3347
}

tests/PathResolver/UnderscoreOperationPathResolverTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,26 @@
1515

1616
use ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver;
1717

18+
/**
19+
* @author Guilhem N. <egetick@gmail.com>
20+
*/
1821
class UnderscoreOperationPathResolverTest extends \PHPUnit_Framework_TestCase
1922
{
2023
public function testResolveCollectionOperationPath()
2124
{
2225
$underscoreOperationPathResolver = new UnderscoreOperationPathResolver();
2326

27+
$this->assertSame('/short_names.{_format}', $underscoreOperationPathResolver->resolveOperationPath('ShortName', [], true, 'get'));
28+
}
29+
30+
/**
31+
* @group legacy
32+
* @expectedDeprecation Method ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver::resolveOperationPath() will have a 4th `string $operationName = null` argument in version 3.0. Not defining it is deprecated since 2.0.10.
33+
*/
34+
public function testLegacyResolveOperationPath()
35+
{
36+
$underscoreOperationPathResolver = new UnderscoreOperationPathResolver();
37+
2438
$this->assertSame('/short_names.{_format}', $underscoreOperationPathResolver->resolveOperationPath('ShortName', [], true));
2539
}
2640
}

0 commit comments

Comments
 (0)