mirror of
https://github.com/retailcrm/NelmioApiDocBundle.git
synced 2025-02-09 02:59:27 +03:00
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:
parent
aa8dcf06d8
commit
7d9573ddf6
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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]);
|
||||||
|
|
||||||
|
@ -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 = '';
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"})
|
||||||
|
@ -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',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user