Skip to content

Commit

Permalink
Merge pull request api-platform#1192 from meyerbaptiste/fix_swagger_c…
Browse files Browse the repository at this point in the history
…ustom_operation_path

Fix path for custom operation with Swagger UI
  • Loading branch information
dunglas authored Jun 26, 2017
2 parents f88d3d0 + 7114ebb commit fd1a553
Show file tree
Hide file tree
Showing 18 changed files with 404 additions and 105 deletions.
2 changes: 1 addition & 1 deletion src/Api/OperationTypeDeprecationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* Because we introduced a third type in API Platform 2.1, we're using a string with OperationType constants:
* - OperationType::ITEM
* - OperationType::COLLECTION
* - OperationType::SUBCOLLECTION
* - OperationType::SUBRESOURCE
*
* @internal
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private function getContext(Request $request, Documentation $documentation): arr

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

$swaggerData['oauth'] = [
Expand Down
2 changes: 1 addition & 1 deletion src/Bridge/Symfony/Bundle/Resources/config/swagger.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<argument type="service" id="api_platform.resource_class_resolver" />
<argument type="service" id="api_platform.operation_method_resolver" />
<argument type="service" id="api_platform.operation_path_resolver" />
<argument type="service" id="api_platform.router" />
<argument>null</argument>
<argument type="service" id="api_platform.filter_locator" />
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
<argument>%api_platform.oauth.enabled%</argument>
Expand Down
29 changes: 10 additions & 19 deletions src/Bridge/Symfony/Routing/ApiLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
*/
final class ApiLoader extends Loader
{
/**
* @deprecated since version 2.1, to be removed in 3.0. Use {@see RouteNameGenerator::ROUTE_NAME_PREFIX} instead.
*/
const ROUTE_NAME_PREFIX = 'api_';
const DEFAULT_ACTION_PATTERN = 'api_platform.action.';
const SUBRESOURCE_SUFFIX = '_get_subresource';
Expand Down Expand Up @@ -166,13 +169,13 @@ private function computeSubresourceOperations(RouteCollection $routeCollection,
$resourceRouteName = $this->routeNameResolver($rootShortname);

$operation['identifiers'] = [['id', $rootResourceClass]];
$operation['route_name'] = sprintf('%s%s_%s%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $propertyName, self::SUBRESOURCE_SUFFIX);
$operation['path'] = $this->operationPathResolver->resolveOperationPath($rootShortname, $operation, OperationType::SUBRESOURCE);
$operation['route_name'] = sprintf('%s%s_%s%s', RouteNameGenerator::ROUTE_NAME_PREFIX, $resourceRouteName, $propertyName, self::SUBRESOURCE_SUFFIX);
$operation['path'] = $this->operationPathResolver->resolveOperationPath($rootShortname, $operation, OperationType::SUBRESOURCE, $operation['route_name']);
} else {
$operation['identifiers'] = $parentOperation['identifiers'];
$operation['identifiers'][] = [$parentOperation['property'], $resourceClass];
$operation['route_name'] = str_replace(self::SUBRESOURCE_SUFFIX, "_$propertyName".self::SUBRESOURCE_SUFFIX, $parentOperation['route_name']);
$operation['path'] = $this->operationPathResolver->resolveOperationPath($parentOperation['path'], $operation, OperationType::SUBRESOURCE);
$operation['path'] = $this->operationPathResolver->resolveOperationPath($parentOperation['path'], $operation, OperationType::SUBRESOURCE, $operation['route_name']);
}

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

$controller = $operation['controller'] ?? null;
$actionName = sprintf('%s_%s', strtolower($operation['method']), $operationType);

if (null === $controller) {
$controller = self::DEFAULT_ACTION_PATTERN.$actionName;
if (null === $controller = $operation['controller'] ?? null) {
$controller = sprintf('%s%s_%s', self::DEFAULT_ACTION_PATTERN, strtolower($operation['method']), $operationType);

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

if ($operationName !== strtolower($operation['method'])) {
$actionName = sprintf('%s_%s', $operationName, $operationType);
}

$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType);

$resourceRouteName = $this->routeNameResolver($resourceShortName);
$routeName = sprintf('%s%s_%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $actionName);

$route = new Route(
$path,
$this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName),
[
'_controller' => $controller,
'_format' => null,
Expand All @@ -280,6 +271,6 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
[$operation['method']]
);

$routeCollection->add($routeName, $route);
$routeCollection->add(RouteNameGenerator::generate($operationName, $resourceShortName, $operationType), $route);
}
}
61 changes: 61 additions & 0 deletions src/Bridge/Symfony/Routing/RouteNameGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Bridge\Symfony\Routing;

use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Api\OperationTypeDeprecationHelper;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use Doctrine\Common\Util\Inflector;

/**
* Generates the Symfony route name associated with an operation name and a resource short name.
*
* @internal
*
* @author Baptiste Meyer <[email protected]>
*/
class RouteNameGenerator
{
const ROUTE_NAME_PREFIX = 'api_';

private function __construct()
{
}

/**
* Generates a Symfony route name.
*
* @param string $operationName
* @param string $resourceShortName
* @param string|bool $operationType
*
* @throws InvalidArgumentException
*
* @return string
*/
public static function generate(string $operationName, string $resourceShortName, $operationType): string
{
if (OperationType::SUBRESOURCE === $operationType = OperationTypeDeprecationHelper::getOperationType($operationType)) {
throw new InvalidArgumentException(sprintf('%s::SUBRESOURCE is not supported as operation type by %s().', OperationType::class, __METHOD__));
}

return sprintf(
'%s%s_%s_%s',
static::ROUTE_NAME_PREFIX,
Inflector::pluralize(Inflector::tableize($resourceShortName)),
$operationName,
$operationType
);
}
}
31 changes: 25 additions & 6 deletions src/Bridge/Symfony/Routing/RouterOperationPathResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace ApiPlatform\Core\Bridge\Symfony\Routing;

use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Api\OperationTypeDeprecationHelper;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
use Symfony\Component\Routing\RouterInterface;
Expand All @@ -38,15 +40,32 @@ public function __construct(RouterInterface $router, OperationPathResolverInterf
*
* @throws InvalidArgumentException
*/
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType): string
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
{
if (!isset($operation['route_name'])) {
return $this->deferred->resolveOperationPath($resourceShortName, $operation, $operationType);
if (func_num_args() >= 4) {
$operationName = func_get_arg(3);
} else {
@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);

$operationName = null;
}

if (OperationType::SUBRESOURCE === $operationType = OperationTypeDeprecationHelper::getOperationType($operationType)) {
return $this->deferred->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
}

if (isset($operation['route_name'])) {
$routeName = $operation['route_name'];
} elseif (null !== $operationName) {
$routeName = RouteNameGenerator::generate($operationName, $resourceShortName, $operationType);
} else {
return $this->deferred->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
}

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

return $route->getPath();
Expand Down
12 changes: 10 additions & 2 deletions src/PathResolver/CustomOperationPathResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,20 @@ public function __construct(OperationPathResolverInterface $deferred)
/**
* {@inheritdoc}
*/
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType): string
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
{
if (func_num_args() >= 4) {
$operationName = func_get_arg(3);
} else {
@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);

$operationName = null;
}

if (isset($operation['path'])) {
return $operation['path'];
}

return $this->deferred->resolveOperationPath($resourceShortName, $operation, OperationTypeDeprecationHelper::getOperationType($operationType));
return $this->deferred->resolveOperationPath($resourceShortName, $operation, OperationTypeDeprecationHelper::getOperationType($operationType), $operationName);
}
}
6 changes: 5 additions & 1 deletion src/PathResolver/DashOperationPathResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ final class DashOperationPathResolver implements OperationPathResolverInterface
/**
* {@inheritdoc}
*/
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType): string
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
{
if (func_num_args() < 4) {
@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);
}

$operationType = OperationTypeDeprecationHelper::getOperationType($operationType);

if ($operationType === OperationType::SUBRESOURCE && 1 < count($operation['identifiers'])) {
Expand Down
3 changes: 2 additions & 1 deletion src/PathResolver/OperationPathResolverInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ interface OperationPathResolverInterface
* @param array $operation The operation metadata
* @param string|bool $operationType One of the constants defined in ApiPlatform\Core\Api\OperationType
* If the property is a boolean, true represents OperationType::COLLECTION, false is for OperationType::ITEM
* @param string $operationName The operation name
*
* @return string
*/
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType): string;
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string;
}
6 changes: 5 additions & 1 deletion src/PathResolver/UnderscoreOperationPathResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ final class UnderscoreOperationPathResolver implements OperationPathResolverInte
/**
* {@inheritdoc}
*/
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType): string
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
{
if (func_num_args() < 4) {
@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);
}

$operationType = OperationTypeDeprecationHelper::getOperationType($operationType);

if ($operationType === OperationType::SUBRESOURCE && 1 < count($operation['identifiers'])) {
Expand Down
24 changes: 14 additions & 10 deletions src/Swagger/Serializer/DocumentationNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ final class DocumentationNormalizer implements NormalizerInterface
private $resourceClassResolver;
private $operationMethodResolver;
private $operationPathResolver;
private $urlGenerator;
private $nameConverter;
private $oauthEnabled;
private $oauthType;
Expand All @@ -63,8 +62,12 @@ final class DocumentationNormalizer implements NormalizerInterface
/**
* @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection
*/
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 = [])
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 = [])
{
if ($urlGenerator) {
@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);
}

$this->setFilterLocator($filterLocator, true);

$this->resourceMetadataFactory = $resourceMetadataFactory;
Expand All @@ -73,7 +76,6 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
$this->resourceClassResolver = $resourceClassResolver;
$this->operationMethodResolver = $operationMethodResolver;
$this->operationPathResolver = $operationPathResolver;
$this->urlGenerator = $urlGenerator;
$this->nameConverter = $nameConverter;
$this->oauthEnabled = $oauthEnabled;
$this->oauthType = $oauthType;
Expand Down Expand Up @@ -103,7 +105,7 @@ public function normalize($object, $format = null, array $context = [])
$definitions->ksort();
$paths->ksort();

return $this->computeDoc($object, $definitions, $paths);
return $this->computeDoc($object, $definitions, $paths, $context);
}

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

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

$paths[$path][strtolower($method)] = $this->getPathOperation($operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions);
Expand All @@ -140,14 +142,15 @@ private function addPaths(\ArrayObject $paths, \ArrayObject $definitions, string
* @see https://github.com/OAI/OpenAPI-Specification/issues/93
*
* @param string $resourceShortName
* @param string $operationName
* @param array $operation
* @param string $operationType
*
* @return string
*/
private function getPath(string $resourceShortName, array $operation, string $operationType): string
private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string
{
$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType);
$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
if ('.{_format}' === substr($path, -10)) {
$path = substr($path, 0, -10);
}
Expand Down Expand Up @@ -211,7 +214,7 @@ private function updateGetOperation(\ArrayObject $pathOperation, array $mimeType

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

if ($operationType === OperationType::COLLECTION || $operationType === OperationType::SUBRESOURCE) {
if ($operationType === OperationType::COLLECTION) {
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName);
$pathOperation['responses'] ?? $pathOperation['responses'] = [
'200' => [
Expand Down Expand Up @@ -526,14 +529,15 @@ private function getType(string $type, bool $isCollection, string $className = n
* @param Documentation $documentation
* @param \ArrayObject $definitions
* @param \ArrayObject $paths
* @param array $context
*
* @return array
*/
private function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths): array
private function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
{
$doc = [
'swagger' => self::SWAGGER_VERSION,
'basePath' => $this->urlGenerator->generate('api_entrypoint'),
'basePath' => $context['base_url'] ?? '/',
'info' => [
'title' => $documentation->getTitle(),
'version' => $documentation->getVersion(),
Expand Down
Loading

0 comments on commit fd1a553

Please sign in to comment.