Move the OpenApi processing to ApiDocGenerator (#1671)

* Move the OpenApi processing to ApiDocGenerator

* Temporary fix for https://github.com/zircote/swagger-php/pull/791

* Stop using the ModelRegistry in OpenApiPhpDescriber
This commit is contained in:
Guilhem Niot 2020-07-06 19:50:34 +02:00 committed by GitHub
parent aa8dcf06d8
commit 7d9573ddf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 49 additions and 90 deletions

View File

@ -15,6 +15,8 @@ use Nelmio\ApiDocBundle\Describer\DescriberInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister;
use OpenApi\Analysis;
use OpenApi\Annotations\OpenApi; use OpenApi\Annotations\OpenApi;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
@ -38,6 +40,9 @@ final class ApiDocGenerator
/** @var string[] */ /** @var string[] */
private $alternativeNames = []; private $alternativeNames = [];
/** @var string[] */
private $mediaTypes = ['json'];
/** /**
* @param DescriberInterface[]|iterable $describers * @param DescriberInterface[]|iterable $describers
* @param ModelDescriberInterface[]|iterable $modelDescribers * @param ModelDescriberInterface[]|iterable $modelDescribers
@ -55,6 +60,11 @@ final class ApiDocGenerator
$this->alternativeNames = $alternativeNames; $this->alternativeNames = $alternativeNames;
} }
public function setMediaTypes(array $mediaTypes)
{
$this->mediaTypes = $mediaTypes;
}
public function generate(): OpenApi public function generate(): OpenApi
{ {
if (null !== $this->openApi) { if (null !== $this->openApi) {
@ -77,8 +87,20 @@ final class ApiDocGenerator
$describer->describe($this->openApi); $describer->describe($this->openApi);
} }
$analysis = new Analysis();
$analysis->addAnnotation($this->openApi, null);
// Register model annotations
$modelRegister = new ModelRegister($modelRegistry, $this->mediaTypes);
$modelRegister($analysis);
// Calculate the associated schemas
$modelRegistry->registerSchemas(); $modelRegistry->registerSchemas();
$analysis->process();
$analysis->validate();
if (isset($item)) { if (isset($item)) {
$this->cacheItemPool->save($item->set($this->openApi)); $this->cacheItemPool->save($item->set($this->openApi));
} }

View File

@ -68,6 +68,7 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
$container->register(sprintf('nelmio_api_doc.generator.%s', $area), ApiDocGenerator::class) $container->register(sprintf('nelmio_api_doc.generator.%s', $area), ApiDocGenerator::class)
->setPublic(true) ->setPublic(true)
->addMethodCall('setAlternativeNames', [$nameAliases]) ->addMethodCall('setAlternativeNames', [$nameAliases])
->addMethodCall('setMediaTypes', [$config['media_types']])
->setArguments([ ->setArguments([
new TaggedIteratorArgument(sprintf('nelmio_api_doc.describer.%s', $area)), new TaggedIteratorArgument(sprintf('nelmio_api_doc.describer.%s', $area)),
new TaggedIteratorArgument('nelmio_api_doc.model_describer'), new TaggedIteratorArgument('nelmio_api_doc.model_describer'),
@ -89,7 +90,6 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
new Reference('nelmio_api_doc.controller_reflector'), new Reference('nelmio_api_doc.controller_reflector'),
new Reference('annotations.reader'), // We cannot use the cached version of the annotation reader since the construction of the annotations is context dependant... new Reference('annotations.reader'), // We cannot use the cached version of the annotation reader since the construction of the annotations is context dependant...
new Reference('logger'), new Reference('logger'),
$config['media_types'],
]) ])
->addTag(sprintf('nelmio_api_doc.describer.%s', $area), ['priority' => -200]); ->addTag(sprintf('nelmio_api_doc.describer.%s', $area), ['priority' => -200]);

View File

@ -34,12 +34,14 @@ final class DefaultDescriber implements DescriberInterface
} }
// Paths // Paths
$paths = OA\UNDEFINED === $api->paths ? [] : $api->paths; if (OA\UNDEFINED === $api->paths) {
foreach ($paths as $path) { $api->paths = [];
}
foreach ($api->paths as $path) {
foreach (Util::OPERATIONS as $method) { foreach (Util::OPERATIONS as $method) {
/** @var OA\Operation $operation */ /** @var OA\Operation $operation */
$operation = $path->{$method}; $operation = $path->{$method};
if (OA\UNDEFINED !== $operation && null !== $operation && empty($operation->responses ?? [])) { if (OA\UNDEFINED !== $operation && null !== $operation && (OA\UNDEFINED === $operation->responses || empty($operation->responses))) {
/** @var OA\Response $response */ /** @var OA\Response $response */
$response = Util::getIndexedCollectionItem($operation, OA\Response::class, 'default'); $response = Util::getIndexedCollectionItem($operation, OA\Response::class, 'default');
$response->description = ''; $response->description = '';

View File

@ -14,12 +14,9 @@ namespace Nelmio\ApiDocBundle\Describer;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\Annotation\Operation; use Nelmio\ApiDocBundle\Annotation\Operation;
use Nelmio\ApiDocBundle\Annotation\Security; use Nelmio\ApiDocBundle\Annotation\Security;
use Nelmio\ApiDocBundle\OpenApiPhp\AddDefaults;
use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\Util\ControllerReflector; use Nelmio\ApiDocBundle\Util\ControllerReflector;
use OpenApi\Analyser; use OpenApi\Analyser;
use OpenApi\Analysis;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
@ -28,49 +25,25 @@ use Symfony\Component\Routing\RouteCollection;
// Help opcache.preload discover Swagger\Annotations\Swagger // Help opcache.preload discover Swagger\Annotations\Swagger
class_exists(OA\OpenApi::class); class_exists(OA\OpenApi::class);
final class OpenApiPhpDescriber implements ModelRegistryAwareInterface final class OpenApiPhpDescriber
{ {
use ModelRegistryAwareTrait;
private $routeCollection; private $routeCollection;
private $controllerReflector; private $controllerReflector;
private $annotationReader; private $annotationReader;
private $logger; private $logger;
private $mediaTypes;
private $overwrite; private $overwrite;
public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, Reader $annotationReader, LoggerInterface $logger, array $mediaTypes, bool $overwrite = false) public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, Reader $annotationReader, LoggerInterface $logger, bool $overwrite = false)
{ {
$this->routeCollection = $routeCollection; $this->routeCollection = $routeCollection;
$this->controllerReflector = $controllerReflector; $this->controllerReflector = $controllerReflector;
$this->annotationReader = $annotationReader; $this->annotationReader = $annotationReader;
$this->logger = $logger; $this->logger = $logger;
$this->mediaTypes = $mediaTypes;
$this->overwrite = $overwrite; $this->overwrite = $overwrite;
} }
public function describe(OA\OpenApi $api) public function describe(OA\OpenApi $api)
{ {
$analysis = $this->getAnnotations($api);
$analysis->process($this->getProcessors());
$analysis->validate();
}
private function getProcessors(): array
{
$processors = [
new AddDefaults(),
new ModelRegister($this->modelRegistry, $this->mediaTypes),
];
return array_merge($processors, Analysis::processors());
}
private function getAnnotations(OA\OpenApi $api): Analysis
{
$analysis = new Analysis();
$analysis->openapi = $api;
$classAnnotations = []; $classAnnotations = [];
/** @var \ReflectionMethod $method */ /** @var \ReflectionMethod $method */
@ -150,9 +123,6 @@ final class OpenApiPhpDescriber implements ModelRegistryAwareInterface
continue; continue;
} }
// Registers new annotations
$analysis->addAnnotations($implicitAnnotations, null);
foreach ($httpMethods as $httpMethod) { foreach ($httpMethods as $httpMethod) {
$operation = Util::getOperation($path, $httpMethod); $operation = Util::getOperation($path, $httpMethod);
$operation->merge($implicitAnnotations); $operation->merge($implicitAnnotations);
@ -162,8 +132,6 @@ final class OpenApiPhpDescriber implements ModelRegistryAwareInterface
// Reset the Analyser after the parsing // Reset the Analyser after the parsing
Analyser::$context = null; Analyser::$context = null;
return $analysis;
} }
private function getMethodsToParse(): \Generator private function getMethodsToParse(): \Generator

View File

@ -59,25 +59,26 @@ class SymfonyConstraintAnnotationReader
$this->schema->required = array_values(array_unique($existingRequiredFields)); $this->schema->required = array_values(array_unique($existingRequiredFields));
} elseif ($annotation instanceof Assert\Length) { } elseif ($annotation instanceof Assert\Length) {
$property->minLength = $annotation->min; $property->minLength = (int) $annotation->min;
$property->maxLength = $annotation->max; $property->maxLength = (int) $annotation->max;
} elseif ($annotation instanceof Assert\Regex) { } elseif ($annotation instanceof Assert\Regex) {
$this->appendPattern($property, $annotation->getHtmlPattern()); $this->appendPattern($property, $annotation->getHtmlPattern());
} elseif ($annotation instanceof Assert\Count) { } elseif ($annotation instanceof Assert\Count) {
$property->minItems = $annotation->min; $property->minItems = (int) $annotation->min;
$property->maxItems = $annotation->max; $property->maxItems = (int) $annotation->max;
} elseif ($annotation instanceof Assert\Choice) { } elseif ($annotation instanceof Assert\Choice) {
$values = $annotation->callback ? call_user_func(is_array($annotation->callback) ? $annotation->callback : [$reflectionProperty->class, $annotation->callback]) : $annotation->choices; $values = $annotation->callback ? call_user_func(is_array($annotation->callback) ? $annotation->callback : [$reflectionProperty->class, $annotation->callback]) : $annotation->choices;
$property->enum = array_values($values); $property->enum = array_values($values);
} elseif ($annotation instanceof Assert\Expression) { } elseif ($annotation instanceof Assert\Expression) {
$this->appendPattern($property, $annotation->message); $this->appendPattern($property, $annotation->message);
} elseif ($annotation instanceof Assert\Range) { } elseif ($annotation instanceof Assert\Range) {
$property->minimum = $annotation->min; $property->minimum = (int) $annotation->min;
$property->maximum = $annotation->max; $property->maximum = (int) $annotation->max;
} elseif ($annotation instanceof Assert\LessThan) { } elseif ($annotation instanceof Assert\LessThan) {
$property->exclusiveMaximum= $annotation->value; $property->exclusiveMaximum = true;
$property->maximum = (int) $annotation->value;
} elseif ($annotation instanceof Assert\LessThanOrEqual) { } elseif ($annotation instanceof Assert\LessThanOrEqual) {
$property->maximum = $annotation->value; $property->maximum = (int) $annotation->value;
} }
} }
} }

View File

@ -1,39 +0,0 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\OpenApiPhp;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Context;
/**
* Add defaults to fix default warnings.
*
* @internal
*/
final class AddDefaults
{
public function __invoke(Analysis $analysis)
{
if ($analysis->getAnnotationsOfType(OA\Info::class)) {
return;
}
if (($annotations = $analysis->getAnnotationsOfType(OA\OpenApi::class)) && OA\UNDEFINED !== $annotations[0]->info) {
return;
}
if (OA\UNDEFINED !== $analysis->openapi->info) {
return;
}
$analysis->addAnnotation(new OA\Info(['title' => '', 'version' => '0.0.0', '_context' => new Context(['generated' => true])]), null);
}
}

View File

@ -360,6 +360,8 @@ final class Util
*/ */
public static function createContext(array $properties = [], Context $parent = null): Context public static function createContext(array $properties = [], Context $parent = null): Context
{ {
$properties['comment'] = ''; // TODO: remove this when https://github.com/zircote/swagger-php/commit/708a25208797ca05ebeae572bbccad8b13de14d8 is released
return new Context($properties, $parent); return new Context($properties, $parent);
} }

View File

@ -27,7 +27,7 @@ class ObjectPropertyDescriber implements PropertyDescriberInterface, ModelRegist
if ($types[0]->isNullable()) { if ($types[0]->isNullable()) {
$property->nullable = true; $property->nullable = true;
$property->allOf = [['$ref' => $this->modelRegistry->register(new Model($type, $groups))]]; $property->allOf = [new OA\Schema(['ref' => $this->modelRegistry->register(new Model($type, $groups))])];
return; return;
} }

View File

@ -23,7 +23,7 @@ class ApiDocGeneratorTest extends TestCase
$adapter = new ArrayAdapter(); $adapter = new ArrayAdapter();
$generator = new ApiDocGenerator([new DefaultDescriber()], [], $adapter); $generator = new ApiDocGenerator([new DefaultDescriber()], [], $adapter);
$this->assertEquals($generator->generate(), $adapter->getItem('openapi_doc')->get()); $this->assertEquals(json_encode($generator->generate()), json_encode($adapter->getItem('openapi_doc')->get()));
} }
public function testCacheWithCustomId() public function testCacheWithCustomId()
@ -31,6 +31,6 @@ class ApiDocGeneratorTest extends TestCase
$adapter = new ArrayAdapter(); $adapter = new ArrayAdapter();
$generator = new ApiDocGenerator([new DefaultDescriber()], [], $adapter, 'custom_id'); $generator = new ApiDocGenerator([new DefaultDescriber()], [], $adapter, 'custom_id');
$this->assertEquals($generator->generate(), $adapter->getItem('custom_id')->get()); $this->assertEquals(json_encode($generator->generate()), json_encode($adapter->getItem('custom_id')->get()));
} }
} }

View File

@ -30,10 +30,12 @@ use Symfony\Component\Routing\Annotation\Route;
class ApiController class ApiController
{ {
/** /**
* @OA\Response( * @OA\Get(
* @OA\Response(
* response="200", * response="200",
* description="Success", * description="Success",
* @Model(type=Article::class, groups={"light"})) * @Model(type=Article::class, groups={"light"}))
* )
* ) * )
* @OA\Parameter(ref="#/components/parameters/test") * @OA\Parameter(ref="#/components/parameters/test")
* @Route("/article/{id}", methods={"GET"}) * @Route("/article/{id}", methods={"GET"})

View File

@ -383,7 +383,8 @@ class FunctionalTest extends WebTestCase
], ],
'propertyLessThan' => [ 'propertyLessThan' => [
'type' => 'integer', 'type' => 'integer',
'exclusiveMaximum' => 42, 'exclusiveMaximum' => true,
'maximum' => 42,
], ],
'propertyLessThanOrEqual' => [ 'propertyLessThanOrEqual' => [
'type' => 'integer', 'type' => 'integer',