OpenApi 3 Support (#1623)

* Initial pass for OA3 upgrade

* Fix Util Tests

* Fix first batch of Unit Tests. Up to Model

* Another batch of fixed tests

* Update annotations

* Convert Model & Property Describers

* Update tests, Fix RouteDescribers, FIx additional bugs

* Another batch of updates

* Another batch of fixed Functional Tests

* Fix FunctionalTest tests

* Fix Bazinga Tests

* FIx FOS Rest

* Fix JMS TEsts & describers

* Fix all Tests

* Fix few stuff from own CR

* CS Fixes

* CS Fixes 2

* CS Fixes 3

* CS Fixes 4

* Remove collection bug

* Updates after first CRs

* CS

* Drop support for SF3

* Update the docs

* Add an upgrade guide

* misc doc fixes

* Configurable media types

* Code Style Fixes

* Don't use ::$ref for @Response and @RequestBody

* Fix upgrading guide

* Fix OA case

Co-authored-by: Filip Benčo <filip.benco@websupport.sk>
Co-authored-by: Guilhem Niot <guilhem.niot@gmail.com>
Co-authored-by: Mantis Development <mantis@users.noreply.github.com>
This commit is contained in:
Filip Benčo 2020-05-28 13:19:11 +02:00 committed by GitHub
parent 2a78b42a94
commit 78664ef9ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 2845 additions and 1329 deletions

View File

@ -18,8 +18,6 @@ matrix:
include: include:
- php: 7.1 - php: 7.1
env: COMPOSER_FLAGS="--prefer-lowest" env: COMPOSER_FLAGS="--prefer-lowest"
- php: 7.2
env: SYMFONY_VERSION=^3.4
- php: 7.3 - php: 7.3
env: SYMFONY_VERSION=^4.0 env: SYMFONY_VERSION=^4.0
- php: 7.3 - php: 7.3

View File

@ -11,7 +11,8 @@
namespace Nelmio\ApiDocBundle\Annotation; namespace Nelmio\ApiDocBundle\Annotation;
use Swagger\Annotations\AbstractAnnotation; use OpenApi\Annotations\AbstractAnnotation;
use OpenApi\Annotations\Parameter;
/** /**
* @Annotation * @Annotation
@ -28,8 +29,7 @@ final class Model extends AbstractAnnotation
public static $_required = ['type']; public static $_required = ['type'];
public static $_parents = [ public static $_parents = [
'Swagger\Annotations\Parameter', Parameter::class,
'Swagger\Annotations\Response',
]; ];
/** /**

View File

@ -11,7 +11,7 @@
namespace Nelmio\ApiDocBundle\Annotation; namespace Nelmio\ApiDocBundle\Annotation;
use Swagger\Annotations\Operation as BaseOperation; use OpenApi\Annotations\Operation as BaseOperation;
/** /**
* @Annotation * @Annotation

View File

@ -11,7 +11,7 @@
namespace Nelmio\ApiDocBundle\Annotation; namespace Nelmio\ApiDocBundle\Annotation;
use Swagger\Annotations\AbstractAnnotation; use OpenApi\Annotations\AbstractAnnotation;
/** /**
* @Annotation * @Annotation

View File

@ -11,23 +11,31 @@
namespace Nelmio\ApiDocBundle; namespace Nelmio\ApiDocBundle;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Describer\DescriberInterface; 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 OpenApi\Annotations\OpenApi;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
final class ApiDocGenerator final class ApiDocGenerator
{ {
private $swagger; /** @var OpenApi */
private $openApi;
/** @var iterable|DescriberInterface[] */
private $describers; private $describers;
/** @var iterable|ModelDescriberInterface[] */
private $modelDescribers; private $modelDescribers;
/** @var CacheItemPoolInterface|null */
private $cacheItemPool; private $cacheItemPool;
/** @var string|null */
private $cacheItemId;
/** @var string[] */
private $alternativeNames = []; private $alternativeNames = [];
/** /**
@ -47,34 +55,34 @@ final class ApiDocGenerator
$this->alternativeNames = $alternativeNames; $this->alternativeNames = $alternativeNames;
} }
public function generate(): Swagger public function generate(): OpenApi
{ {
if (null !== $this->swagger) { if (null !== $this->openApi) {
return $this->swagger; return $this->openApi;
} }
if ($this->cacheItemPool) { if ($this->cacheItemPool) {
$item = $this->cacheItemPool->getItem($this->cacheItemId ?? 'swagger_doc'); $item = $this->cacheItemPool->getItem($this->cacheItemId ?? 'openapi_doc');
if ($item->isHit()) { if ($item->isHit()) {
return $this->swagger = $item->get(); return $this->openApi = $item->get();
} }
} }
$this->swagger = new Swagger(); $this->openApi = new OpenApi([]);
$modelRegistry = new ModelRegistry($this->modelDescribers, $this->swagger, $this->alternativeNames); $modelRegistry = new ModelRegistry($this->modelDescribers, $this->openApi, $this->alternativeNames);
foreach ($this->describers as $describer) { foreach ($this->describers as $describer) {
if ($describer instanceof ModelRegistryAwareInterface) { if ($describer instanceof ModelRegistryAwareInterface) {
$describer->setModelRegistry($modelRegistry); $describer->setModelRegistry($modelRegistry);
} }
$describer->describe($this->swagger); $describer->describe($this->openApi);
} }
$modelRegistry->registerDefinitions(); $modelRegistry->registerSchemas();
if (isset($item)) { if (isset($item)) {
$this->cacheItemPool->save($item->set($this->swagger)); $this->cacheItemPool->save($item->set($this->openApi));
} }
return $this->swagger; return $this->openApi;
} }
} }

View File

@ -1,8 +1,12 @@
CHANGELOG CHANGELOG
========= =========
3.3.0 (unreleased) 4.0.0
------------------ -----
* Added support of OpenAPI 3.0. The internals were completely reworked and this version introduces BC breaks.
3.3.0
-----
* Usage of Google Fonts was removed. System fonts `serif` / `sans` will be used instead. * Usage of Google Fonts was removed. System fonts `serif` / `sans` will be used instead.
This can lead to a different look on different operating systems. This can lead to a different look on different operating systems.

View File

@ -12,6 +12,8 @@
namespace Nelmio\ApiDocBundle\Controller; namespace Nelmio\ApiDocBundle\Controller;
use Nelmio\ApiDocBundle\ApiDocGenerator; use Nelmio\ApiDocBundle\ApiDocGenerator;
use OpenApi\Annotations\OpenApi;
use OpenApi\Annotations\Server;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
@ -47,14 +49,11 @@ final class DocumentationController
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.', $area)); throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.', $area));
} }
$spec = $this->generatorLocator->get($area)->generate()->toArray(); /** @var OpenApi $spec */
$spec = $this->generatorLocator->get($area)->generate();
if ('' !== $request->getBaseUrl()) { if ('' !== $request->getBaseUrl()) {
$spec['basePath'] = $request->getBaseUrl(); $spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
}
if (empty($spec['host'])) {
$spec['host'] = $request->getHost();
} }
return new JsonResponse($spec); return new JsonResponse($spec);

View File

@ -12,6 +12,8 @@
namespace Nelmio\ApiDocBundle\Controller; namespace Nelmio\ApiDocBundle\Controller;
use Nelmio\ApiDocBundle\ApiDocGenerator; use Nelmio\ApiDocBundle\ApiDocGenerator;
use OpenApi\Annotations\OpenApi;
use OpenApi\Annotations\Server;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -60,13 +62,18 @@ final class SwaggerUiController
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.%s', $area, $advice)); throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.%s', $area, $advice));
} }
$spec = $this->generatorLocator->get($area)->generate()->toArray(); /** @var OpenApi $spec */
$spec = $this->generatorLocator->get($area)->generate();
if ('' !== $request->getBaseUrl()) { if ('' !== $request->getBaseUrl()) {
$spec['basePath'] = $request->getBaseUrl(); $spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
} }
return new Response( return new Response(
$this->twig->render('@NelmioApiDoc/SwaggerUi/index.html.twig', ['swagger_data' => ['spec' => $spec]]), $this->twig->render(
'@NelmioApiDoc/SwaggerUi/index.html.twig',
['swagger_data' => ['spec' => json_decode($spec->toJson(), true)]]
),
Response::HTTP_OK, Response::HTTP_OK,
['Content-Type' => 'text/html'] ['Content-Type' => 'text/html']
); );

View File

@ -53,6 +53,11 @@ final class Configuration implements ConfigurationInterface
->example(['info' => ['title' => 'My App']]) ->example(['info' => ['title' => 'My App']])
->prototype('variable')->end() ->prototype('variable')->end()
->end() ->end()
->arrayNode('media_types')
->info('List of enabled Media Types')
->defaultValue(['json'])
->prototype('scalar')->end()
->end()
->arrayNode('areas') ->arrayNode('areas')
->info('Filter the routes that are documented') ->info('Filter the routes that are documented')
->defaultValue( ->defaultValue(

View File

@ -15,8 +15,8 @@ use FOS\RestBundle\Controller\Annotations\ParamInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface; use JMS\Serializer\Visitor\SerializationVisitorInterface;
use Nelmio\ApiDocBundle\ApiDocGenerator; use Nelmio\ApiDocBundle\ApiDocGenerator;
use Nelmio\ApiDocBundle\Describer\ExternalDocDescriber; use Nelmio\ApiDocBundle\Describer\ExternalDocDescriber;
use Nelmio\ApiDocBundle\Describer\OpenApiPhpDescriber;
use Nelmio\ApiDocBundle\Describer\RouteDescriber; use Nelmio\ApiDocBundle\Describer\RouteDescriber;
use Nelmio\ApiDocBundle\Describer\SwaggerPhpDescriber;
use Nelmio\ApiDocBundle\ModelDescriber\BazingaHateoasModelDescriber; use Nelmio\ApiDocBundle\ModelDescriber\BazingaHateoasModelDescriber;
use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber; use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber;
use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder;
@ -82,13 +82,14 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
]) ])
->addTag(sprintf('nelmio_api_doc.describer.%s', $area), ['priority' => -400]); ->addTag(sprintf('nelmio_api_doc.describer.%s', $area), ['priority' => -400]);
$container->register(sprintf('nelmio_api_doc.describers.swagger_php.%s', $area), SwaggerPhpDescriber::class) $container->register(sprintf('nelmio_api_doc.describers.openapi_php.%s', $area), OpenApiPhpDescriber::class)
->setPublic(false) ->setPublic(false)
->setArguments([ ->setArguments([
new Reference(sprintf('nelmio_api_doc.routes.%s', $area)), new Reference(sprintf('nelmio_api_doc.routes.%s', $area)),
new Reference('nelmio_api_doc.controller_reflector'), new Reference('nelmio_api_doc.controller_reflector'),
new Reference('annotation_reader'), new Reference('annotation_reader'),
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]);
@ -135,11 +136,16 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
array_map(function ($area) { return new Reference(sprintf('nelmio_api_doc.generator.%s', $area)); }, array_keys($config['areas'])) array_map(function ($area) { return new Reference(sprintf('nelmio_api_doc.generator.%s', $area)); }, array_keys($config['areas']))
)); ));
$container->getDefinition('nelmio_api_doc.model_describers.object')
->setArgument(3, $config['media_types']);
// Import services needed for each library // Import services needed for each library
$loader->load('php_doc.xml'); $loader->load('php_doc.xml');
if (interface_exists(ParamInterface::class)) { if (interface_exists(ParamInterface::class)) {
$loader->load('fos_rest.xml'); $loader->load('fos_rest.xml');
$container->getDefinition('nelmio_api_doc.route_describers.fos_rest')
->setArgument(1, $config['media_types']);
} }
// ApiPlatform support // ApiPlatform support
@ -159,8 +165,9 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
->setPublic(false) ->setPublic(false)
->setArguments([ ->setArguments([
new Reference('jms_serializer.metadata_factory'), new Reference('jms_serializer.metadata_factory'),
$jmsNamingStrategy,
new Reference('annotation_reader'), new Reference('annotation_reader'),
$config['media_types'],
$jmsNamingStrategy,
]) ])
->addTag('nelmio_api_doc.model_describer', ['priority' => 50]); ->addTag('nelmio_api_doc.model_describer', ['priority' => 50]);

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Describer; namespace Nelmio\ApiDocBundle\Describer;
use ApiPlatform\Core\Documentation\Documentation; use ApiPlatform\Core\Documentation\Documentation;
use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class ApiPlatformDescriber extends ExternalDocDescriber final class ApiPlatformDescriber extends ExternalDocDescriber
@ -23,7 +24,12 @@ final class ApiPlatformDescriber extends ExternalDocDescriber
} }
parent::__construct(function () use ($documentation, $normalizer) { parent::__construct(function () use ($documentation, $normalizer) {
$documentation = (array) $normalizer->normalize($documentation); $documentation = (array) $normalizer->normalize(
$documentation,
null,
[DocumentationNormalizer::SPEC_VERSION => 3]
);
unset($documentation['basePath']); unset($documentation['basePath']);
return $documentation; return $documentation;

View File

@ -11,7 +11,8 @@
namespace Nelmio\ApiDocBundle\Describer; namespace Nelmio\ApiDocBundle\Describer;
use EXSyst\Component\Swagger\Swagger; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
/** /**
* Makes the swagger documentation valid even if there are missing fields. * Makes the swagger documentation valid even if there are missing fields.
@ -20,27 +21,28 @@ use EXSyst\Component\Swagger\Swagger;
*/ */
final class DefaultDescriber implements DescriberInterface final class DefaultDescriber implements DescriberInterface
{ {
public function describe(Swagger $api) public function describe(OA\OpenApi $api)
{ {
// Info // Info
$info = $api->getInfo(); /** @var OA\Info $info */
if (null === $info->getTitle()) { $info = Util::getChild($api, OA\Info::class);
$info->setTitle(''); if (OA\UNDEFINED === $info->title) {
$info->title = '';
} }
if (null === $info->getVersion()) { if (OA\UNDEFINED === $info->version) {
$info->setVersion('0.0.0'); $info->version = '0.0.0';
} }
// Paths // Paths
$paths = $api->getPaths(); $paths = OA\UNDEFINED === $api->paths ? [] : $api->paths;
foreach ($paths as $uri => $path) { foreach ($paths as $path) {
foreach ($path->getMethods() as $method) { foreach (Util::OPERATIONS as $method) {
$operation = $path->getOperation($method); /** @var OA\Operation $operation */
$operation = $path->{$method};
// Default Response if (OA\UNDEFINED !== $operation && null !== $operation && empty($operation->responses ?? [])) {
if (0 === iterator_count($operation->getResponses())) { /** @var OA\Response $response */
$defaultResponse = $operation->getResponses()->get('default'); $response = Util::getIndexedCollectionItem($operation, OA\Response::class, 'default');
$defaultResponse->setDescription(''); $response->description = '';
} }
} }
} }

View File

@ -11,9 +11,9 @@
namespace Nelmio\ApiDocBundle\Describer; namespace Nelmio\ApiDocBundle\Describer;
use EXSyst\Component\Swagger\Swagger; use OpenApi\Annotations\OpenApi;
interface DescriberInterface interface DescriberInterface
{ {
public function describe(Swagger $api); public function describe(OpenApi $api);
} }

View File

@ -11,7 +11,8 @@
namespace Nelmio\ApiDocBundle\Describer; namespace Nelmio\ApiDocBundle\Describer;
use EXSyst\Component\Swagger\Swagger; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
class ExternalDocDescriber implements DescriberInterface class ExternalDocDescriber implements DescriberInterface
{ {
@ -29,10 +30,13 @@ class ExternalDocDescriber implements DescriberInterface
$this->overwrite = $overwrite; $this->overwrite = $overwrite;
} }
public function describe(Swagger $api) public function describe(OA\OpenApi $api)
{ {
$externalDoc = $this->getExternalDoc(); $externalDoc = $this->getExternalDoc();
$api->merge($externalDoc, $this->overwrite);
if (!empty($externalDoc)) {
Util::merge($api, $externalDoc, $this->overwrite);
}
} }
private function getExternalDoc() private function getExternalDoc()

View File

@ -0,0 +1,212 @@
<?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\Describer;
use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\Annotation\Operation;
use Nelmio\ApiDocBundle\Annotation\Security;
use Nelmio\ApiDocBundle\OpenApiPhp\AddDefaults;
use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\Util\ControllerReflector;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
// Help opcache.preload discover Swagger\Annotations\Swagger
class_exists(OA\OpenApi::class);
final class OpenApiPhpDescriber implements ModelRegistryAwareInterface
{
use ModelRegistryAwareTrait;
private $routeCollection;
private $controllerReflector;
private $annotationReader;
private $logger;
private $mediaTypes;
private $overwrite;
public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, Reader $annotationReader, LoggerInterface $logger, array $mediaTypes, bool $overwrite = false)
{
$this->routeCollection = $routeCollection;
$this->controllerReflector = $controllerReflector;
$this->annotationReader = $annotationReader;
$this->logger = $logger;
$this->mediaTypes = $mediaTypes;
$this->overwrite = $overwrite;
}
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 = [];
/** @var \ReflectionMethod $method */
foreach ($this->getMethodsToParse() as $method => list($path, $httpMethods)) {
$declaringClass = $method->getDeclaringClass();
if (!array_key_exists($declaringClass->getName(), $classAnnotations)) {
$classAnnotations = array_filter($this->annotationReader->getClassAnnotations($declaringClass), function ($v) {
return $v instanceof OA\AbstractAnnotation;
});
$classAnnotations[$declaringClass->getName()] = $classAnnotations;
}
$annotations = array_filter($this->annotationReader->getMethodAnnotations($method), function ($v) {
return $v instanceof OA\AbstractAnnotation;
});
if (0 === count($annotations)) {
continue;
}
$path = Util::getPath($api, $path);
$path->_context->namespace = $method->getNamespaceName();
$path->_context->class = $declaringClass->getShortName();
$path->_context->method = $method->name;
$path->_context->filename = $method->getFileName();
$nestedContext = Util::createContext(['nested' => $path], $path->_context);
$implicitAnnotations = [];
$mergeProperties = new \stdClass();
foreach (array_merge($annotations, $classAnnotations[$declaringClass->getName()]) as $annotation) {
$annotation->_context = $nestedContext;
if ($annotation instanceof Operation) {
foreach ($httpMethods as $httpMethod) {
$operation = Util::getOperation($path, $httpMethod);
$operation->mergeProperties($annotation);
}
continue;
}
if ($annotation instanceof OA\Operation) {
$operation = Util::getOperation($path, $annotation->method);
$operation->mergeProperties($annotation);
continue;
}
if ($annotation instanceof Security) {
$annotation->validate();
$mergeProperties->security[] = [$annotation->name => []];
continue;
}
if ($annotation instanceof OA\Tag) {
$annotation->validate();
$mergeProperties->tags[] = $annotation->name;
continue;
}
if (
!$annotation instanceof OA\Response &&
!$annotation instanceof OA\RequestBody &&
!$annotation instanceof OA\Parameter &&
!$annotation instanceof OA\ExternalDocumentation
) {
throw new \LogicException(sprintf('Using the annotation "%s" as a root annotation in "%s::%s()" is not allowed.', get_class($annotation), $method->getDeclaringClass()->name, $method->name));
}
foreach ($annotation->_unmerged as $unmergedAnnotation) {
if (!$unmergedAnnotation instanceof OA\JsonContent && !$unmergedAnnotation instanceof OA\XmlContent) {
continue;
}
$unmergedAnnotation->_context->nested = $annotation;
}
$implicitAnnotations[] = $annotation;
}
if (empty($implicitAnnotations) && empty(get_object_vars($mergeProperties))) {
continue;
}
// Registers new annotations
$analysis->addAnnotations($implicitAnnotations, null);
foreach ($httpMethods as $httpMethod) {
$operation = Util::getOperation($path, $httpMethod);
$operation->merge($implicitAnnotations);
$operation->mergeProperties($mergeProperties);
}
}
return $analysis;
}
private function getMethodsToParse(): \Generator
{
foreach ($this->routeCollection->all() as $route) {
if (!$route->hasDefault('_controller')) {
continue;
}
$controller = $route->getDefault('_controller');
$reflectedMethod = $this->controllerReflector->getReflectionMethod($controller);
if (null === $reflectedMethod) {
continue;
}
$path = $this->normalizePath($route->getPath());
$supportedHttpMethods = $this->getSupportedHttpMethods($route);
if (empty($supportedHttpMethods)) {
$this->logger->warning('None of the HTTP methods specified for path {path} are supported by swagger-ui, skipping this path', [
'path' => $path,
]);
continue;
}
yield $reflectedMethod => [$path, $supportedHttpMethods];
}
}
private function getSupportedHttpMethods(Route $route): array
{
$allMethods = Util::OPERATIONS;
$methods = array_map('strtolower', $route->getMethods());
return array_intersect($methods ?: $allMethods, $allMethods);
}
private function normalizePath(string $path): string
{
if ('.{_format}' === substr($path, -10)) {
$path = substr($path, 0, -10);
}
return $path;
}
}

View File

@ -11,10 +11,9 @@
namespace Nelmio\ApiDocBundle\Describer; namespace Nelmio\ApiDocBundle\Describer;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\RouteDescriber\RouteDescriberInterface; use Nelmio\ApiDocBundle\RouteDescriber\RouteDescriberInterface;
use Nelmio\ApiDocBundle\Util\ControllerReflector; use Nelmio\ApiDocBundle\Util\ControllerReflector;
use Symfony\Component\Routing\Route; use OpenApi\Annotations as OA;
use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\RouteCollection;
final class RouteDescriber implements DescriberInterface, ModelRegistryAwareInterface final class RouteDescriber implements DescriberInterface, ModelRegistryAwareInterface
@ -39,7 +38,7 @@ final class RouteDescriber implements DescriberInterface, ModelRegistryAwareInte
$this->routeDescribers = $routeDescribers; $this->routeDescribers = $routeDescribers;
} }
public function describe(Swagger $api) public function describe(OA\OpenApi $api)
{ {
if (0 === count($this->routeDescribers)) { if (0 === count($this->routeDescribers)) {
return; return;

View File

@ -1,276 +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\Describer;
use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Annotation\Operation;
use Nelmio\ApiDocBundle\Annotation\Security;
use Nelmio\ApiDocBundle\SwaggerPhp\AddDefaults;
use Nelmio\ApiDocBundle\SwaggerPhp\ModelRegister;
use Nelmio\ApiDocBundle\Util\ControllerReflector;
use Psr\Log\LoggerInterface;
use Swagger\Analysis;
use Swagger\Annotations\AbstractAnnotation;
use Swagger\Annotations as SWG;
use Swagger\Context;
use Symfony\Component\Routing\RouteCollection;
// Help opcache.preload discover Swagger\Annotations\Swagger
class_exists(SWG\Swagger::class);
final class SwaggerPhpDescriber implements ModelRegistryAwareInterface
{
use ModelRegistryAwareTrait;
private $routeCollection;
private $controllerReflector;
private $annotationReader;
private $logger;
private $overwrite;
public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, Reader $annotationReader, LoggerInterface $logger, bool $overwrite = false)
{
$this->routeCollection = $routeCollection;
$this->controllerReflector = $controllerReflector;
$this->annotationReader = $annotationReader;
$this->logger = $logger;
$this->overwrite = $overwrite;
}
public function describe(Swagger $api)
{
$analysis = $this->getAnnotations($api);
$analysis->process($this->getProcessors());
$analysis->validate();
$api->merge(json_decode(json_encode($analysis->swagger), true), $this->overwrite);
}
private function getProcessors(): array
{
$processors = [
new AddDefaults(),
new ModelRegister($this->modelRegistry),
];
return array_merge($processors, Analysis::processors());
}
private function getAnnotations(Swagger $api): Analysis
{
$analysis = new Analysis();
$analysis->addAnnotation(new class($api) extends SWG\Swagger {
private $api;
public function __construct(Swagger $api)
{
$this->api = $api;
parent::__construct([]);
}
/**
* Support definitions from the config and reference to models.
*/
public function ref($ref)
{
if (0 === strpos($ref, '#/definitions/') && $this->api->getDefinitions()->has(substr($ref, 14))) {
return;
}
if (0 === strpos($ref, '#/parameters/') && isset($this->api->getParameters()[substr($ref, 13)])) {
return;
}
if (0 === strpos($ref, '#/responses/') && $this->api->getResponses()->has(substr($ref, 12))) {
return;
}
parent::ref($ref);
}
}, null);
$operationAnnotations = [
'get' => SWG\Get::class,
'post' => SWG\Post::class,
'put' => SWG\Put::class,
'patch' => SWG\Patch::class,
'delete' => SWG\Delete::class,
'options' => SWG\Options::class,
'head' => SWG\Head::class,
];
$classAnnotations = [];
foreach ($this->getMethodsToParse() as $method => list($path, $httpMethods)) {
$declaringClass = $method->getDeclaringClass();
if (!array_key_exists($declaringClass->getName(), $classAnnotations)) {
$classAnnotations = array_filter($this->annotationReader->getClassAnnotations($declaringClass), function ($v) {
return $v instanceof SWG\AbstractAnnotation;
});
$classAnnotations[$declaringClass->getName()] = $classAnnotations;
}
$annotations = array_filter($this->annotationReader->getMethodAnnotations($method), function ($v) {
return $v instanceof SWG\AbstractAnnotation;
});
if (0 === count($annotations)) {
continue;
}
$context = new Context([
'namespace' => $method->getNamespaceName(),
'class' => $declaringClass->getShortName(),
'method' => $method->name,
'filename' => $method->getFileName(),
]);
$nestedContext = clone $context;
$nestedContext->nested = true;
$implicitAnnotations = [];
$operations = [];
$tags = [];
$security = [];
foreach (array_merge($annotations, $classAnnotations[$declaringClass->getName()]) as $annotation) {
$annotation->_context = $context;
$this->updateNestedAnnotations($annotation, $nestedContext);
if ($annotation instanceof Operation) {
foreach ($httpMethods as $httpMethod) {
$annotationClass = $operationAnnotations[$httpMethod];
$operation = new $annotationClass(['_context' => $context]);
$operation->path = $path;
$operation->mergeProperties($annotation);
$operations[$httpMethod] = $operation;
$analysis->addAnnotation($operation, null);
}
continue;
}
if ($annotation instanceof SWG\Operation) {
if (null === $annotation->path) {
$annotation = clone $annotation;
$annotation->path = $path;
}
$operations[$annotation->method] = $annotation;
$analysis->addAnnotation($annotation, null);
continue;
}
if ($annotation instanceof Security) {
$annotation->validate();
$security[] = [$annotation->name => []];
continue;
}
if ($annotation instanceof SWG\Tag) {
$annotation->validate();
$tags[] = $annotation->name;
continue;
}
if (!$annotation instanceof SWG\Response && !$annotation instanceof SWG\Parameter && !$annotation instanceof SWG\ExternalDocumentation) {
throw new \LogicException(sprintf('Using the annotation "%s" as a root annotation in "%s::%s()" is not allowed. It should probably be nested in a `@SWG\Response` or `@SWG\Parameter` annotation.', get_class($annotation), $method->getDeclaringClass()->name, $method->name));
}
$implicitAnnotations[] = $annotation;
}
if (0 === count($implicitAnnotations) && 0 === count($tags) && 0 === count($security)) {
continue;
}
// Registers new annotations
$analysis->addAnnotations($implicitAnnotations, null);
foreach ($httpMethods as $httpMethod) {
$annotationClass = $operationAnnotations[$httpMethod];
$constructorArg = [
'_context' => $context,
'path' => $path,
'value' => $implicitAnnotations,
];
if (0 !== count($tags)) {
$constructorArg['tags'] = $tags;
}
if (0 !== count($security)) {
$constructorArg['security'] = $security;
}
$operation = new $annotationClass($constructorArg);
if (isset($operations[$httpMethod])) {
$operations[$httpMethod]->mergeProperties($operation);
} else {
$analysis->addAnnotation($operation, null);
}
}
}
return $analysis;
}
private function getMethodsToParse(): \Generator
{
foreach ($this->routeCollection->all() as $route) {
if (!$route->hasDefault('_controller')) {
continue;
}
$controller = $route->getDefault('_controller');
if ($method = $this->controllerReflector->getReflectionMethod($controller)) {
$path = $this->normalizePath($route->getPath());
$httpMethods = $route->getMethods() ?: Swagger::$METHODS;
$httpMethods = array_map('strtolower', $httpMethods);
$supportedHttpMethods = array_intersect($httpMethods, Swagger::$METHODS);
if (empty($supportedHttpMethods)) {
$this->logger->warning('None of the HTTP methods specified for path {path} are supported by swagger-ui, skipping this path', [
'path' => $path,
'methods' => $httpMethods,
]);
continue;
}
yield $method => [$path, $supportedHttpMethods];
}
}
}
private function normalizePath(string $path): string
{
if ('.{_format}' === substr($path, -10)) {
$path = substr($path, 0, -10);
}
return $path;
}
private function updateNestedAnnotations($value, Context $context)
{
if ($value instanceof AbstractAnnotation) {
$value->_context = $context;
} elseif (!is_array($value)) {
return;
}
foreach ($value as $v) {
$this->updateNestedAnnotations($v, $context);
}
}
}

View File

@ -11,10 +11,10 @@
namespace Nelmio\ApiDocBundle\Model; namespace Nelmio\ApiDocBundle\Model;
use EXSyst\Component\Swagger\Schema;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
final class ModelRegistry final class ModelRegistry
@ -36,7 +36,7 @@ final class ModelRegistry
* *
* @internal * @internal
*/ */
public function __construct($modelDescribers, Swagger $api, array $alternativeNames = []) public function __construct($modelDescribers, OA\OpenApi $api, array $alternativeNames = [])
{ {
$this->modelDescribers = $modelDescribers; $this->modelDescribers = $modelDescribers;
$this->api = $api; $this->api = $api;
@ -45,7 +45,7 @@ final class ModelRegistry
foreach (array_reverse($alternativeNames) as $alternativeName => $criteria) { foreach (array_reverse($alternativeNames) as $alternativeName => $criteria) {
$this->alternativeNames[] = $model = new Model(new Type('object', false, $criteria['type']), $criteria['groups']); $this->alternativeNames[] = $model = new Model(new Type('object', false, $criteria['type']), $criteria['groups']);
$this->names[$model->getHash()] = $alternativeName; $this->names[$model->getHash()] = $alternativeName;
$this->api->getDefinitions()->get($alternativeName); Util::getSchema($this->api, $alternativeName);
} }
} }
@ -61,15 +61,15 @@ final class ModelRegistry
} }
// Reserve the name // Reserve the name
$this->api->getDefinitions()->get($this->names[$hash]); Util::getSchema($this->api, $this->names[$hash]);
return '#/definitions/'.$this->names[$hash]; return OA\Components::SCHEMA_REF.$this->names[$hash];
} }
/** /**
* @internal * @internal
*/ */
public function registerDefinitions() public function registerSchemas(): void
{ {
while (count($this->unregistered)) { while (count($this->unregistered)) {
$tmp = []; $tmp = [];
@ -85,7 +85,7 @@ final class ModelRegistry
$modelDescriber->setModelRegistry($this); $modelDescriber->setModelRegistry($this);
} }
if ($modelDescriber->supports($model)) { if ($modelDescriber->supports($model)) {
$schema = new Schema(); $schema = Util::getSchema($this->api, $name);
$modelDescriber->describe($model, $schema); $modelDescriber->describe($model, $schema);
break; break;
@ -95,8 +95,6 @@ final class ModelRegistry
if (null === $schema) { if (null === $schema) {
throw new \LogicException(sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $this->typeToString($model->getType()))); throw new \LogicException(sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $this->typeToString($model->getType())));
} }
$this->api->getDefinitions()->set($name, $schema);
} }
} }
@ -105,18 +103,19 @@ final class ModelRegistry
$this->register($model); $this->register($model);
} }
$this->alternativeNames = []; $this->alternativeNames = [];
$this->registerDefinitions(); $this->registerSchemas();
} }
} }
private function generateModelName(Model $model): string private function generateModelName(Model $model): string
{ {
$definitions = $this->api->getDefinitions();
$name = $base = $this->getTypeShortName($model->getType()); $name = $base = $this->getTypeShortName($model->getType());
$names = array_column(
$this->api->components instanceof OA\Components && is_array($this->api->components->schemas) ? $this->api->components->schemas : [],
'schema'
);
$i = 1; $i = 1;
while ($definitions->has($name)) { while (\in_array($name, $names, true)) {
++$i; ++$i;
$name = $base.$i; $name = $base.$i;
} }

View File

@ -12,8 +12,8 @@
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations; namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\Model\ModelRegistry;
use OpenApi\Annotations as OA;
/** /**
* @internal * @internal
@ -24,34 +24,34 @@ class AnnotationsReader
private $modelRegistry; private $modelRegistry;
private $phpDocReader; private $phpDocReader;
private $swgAnnotationsReader; private $openApiAnnotationsReader;
private $symfonyConstraintAnnotationReader; private $symfonyConstraintAnnotationReader;
public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry) public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes)
{ {
$this->annotationsReader = $annotationsReader; $this->annotationsReader = $annotationsReader;
$this->modelRegistry = $modelRegistry; $this->modelRegistry = $modelRegistry;
$this->phpDocReader = new PropertyPhpDocReader(); $this->phpDocReader = new PropertyPhpDocReader();
$this->swgAnnotationsReader = new SwgAnnotationsReader($annotationsReader, $modelRegistry); $this->openApiAnnotationsReader = new OpenApiAnnotationsReader($annotationsReader, $modelRegistry, $mediaTypes);
$this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($annotationsReader); $this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($annotationsReader);
} }
public function updateDefinition(\ReflectionClass $reflectionClass, Schema $schema) public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): void
{ {
$this->swgAnnotationsReader->updateDefinition($reflectionClass, $schema); $this->openApiAnnotationsReader->updateSchema($reflectionClass, $schema);
$this->symfonyConstraintAnnotationReader->setSchema($schema); $this->symfonyConstraintAnnotationReader->setSchema($schema);
} }
public function getPropertyName(\ReflectionProperty $reflectionProperty, string $default): string public function getPropertyName(\ReflectionProperty $reflectionProperty, string $default): string
{ {
return $this->swgAnnotationsReader->getPropertyName($reflectionProperty, $default); return $this->openApiAnnotationsReader->getPropertyName($reflectionProperty, $default);
} }
public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property, array $serializationGroups = null) public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property, array $serializationGroups = null): void
{ {
$this->phpDocReader->updateProperty($reflectionProperty, $property); $this->phpDocReader->updateProperty($reflectionProperty, $property);
$this->swgAnnotationsReader->updateProperty($reflectionProperty, $property, $serializationGroups); $this->openApiAnnotationsReader->updateProperty($reflectionProperty, $property, $serializationGroups);
$this->symfonyConstraintAnnotationReader->updateProperty($reflectionProperty, $property); $this->symfonyConstraintAnnotationReader->updateProperty($reflectionProperty, $property);
} }
} }

View File

@ -0,0 +1,87 @@
<?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\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Context;
/**
* @internal
*/
class OpenApiAnnotationsReader
{
private $annotationsReader;
private $modelRegister;
public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes)
{
$this->annotationsReader = $annotationsReader;
$this->modelRegister = new ModelRegister($modelRegistry, $mediaTypes);
}
public function updateSchema(\ReflectionClass $reflectionClass, OA\Schema $schema): void
{
/** @var OA\Schema $oaSchema */
if (!$oaSchema = $this->annotationsReader->getClassAnnotation($reflectionClass, OA\Schema::class)) {
return;
}
// Read @Model annotations
$this->modelRegister->__invoke(new Analysis([$oaSchema]));
if (!$oaSchema->validate()) {
return;
}
$schema->mergeProperties($oaSchema);
}
public function getPropertyName(\ReflectionProperty $reflectionProperty, string $default): string
{
/** @var OA\Property $oaProperty */
if (!$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, OA\Property::class)) {
return $default;
}
return OA\UNDEFINED !== $oaProperty->property ? $oaProperty->property : $default;
}
public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property, array $serializationGroups = null): void
{
/** @var OA\Property $oaProperty */
if (!$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, OA\Property::class)) {
return;
}
$declaringClass = $reflectionProperty->getDeclaringClass();
$context = new Context([
'namespace' => $declaringClass->getNamespaceName(),
'class' => $declaringClass->getShortName(),
'property' => $reflectionProperty->name,
'filename' => $declaringClass->getFileName(),
]);
$oaProperty->_context = $context;
// Read @Model annotations
$this->modelRegister->__invoke(new Analysis([$oaProperty]), $serializationGroups);
if (!$oaProperty->validate()) {
return;
}
$property->mergeProperties($oaProperty);
}
}

View File

@ -11,8 +11,7 @@
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations; namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use EXSyst\Component\Swagger\Schema; use OpenApi\Annotations as OA;
use Nelmio\ApiDocBundle\Model\Model;
use phpDocumentor\Reflection\DocBlock\Tags\Var_; use phpDocumentor\Reflection\DocBlock\Tags\Var_;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
@ -33,7 +32,7 @@ class PropertyPhpDocReader
/** /**
* Update the Swagger information with information from the DocBlock comment. * Update the Swagger information with information from the DocBlock comment.
*/ */
public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property) public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property): void
{ {
try { try {
$docBlock = $this->docBlockFactory->create($reflectionProperty); $docBlock = $this->docBlockFactory->create($reflectionProperty);
@ -54,11 +53,11 @@ class PropertyPhpDocReader
} }
} }
} }
if (null === $property->getTitle() && $title) { if (OA\UNDEFINED === $property->title && $title) {
$property->setTitle($title); $property->title = $title;
} }
if (null === $property->getDescription() && $docBlock->getDescription() && $docBlock->getDescription()->render()) { if (OA\UNDEFINED === $property->description && $docBlock->getDescription() && $docBlock->getDescription()->render()) {
$property->setDescription($docBlock->getDescription()->render()); $property->description = $docBlock->getDescription()->render();
} }
} }
} }

View File

@ -1,89 +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\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\SwaggerPhp\ModelRegister;
use Swagger\Analysis;
use Swagger\Annotations\Definition as SwgDefinition;
use Swagger\Annotations\Property as SwgProperty;
use Swagger\Context;
/**
* @internal
*/
class SwgAnnotationsReader
{
private $annotationsReader;
private $modelRegister;
public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry)
{
$this->annotationsReader = $annotationsReader;
$this->modelRegister = new ModelRegister($modelRegistry);
}
public function updateDefinition(\ReflectionClass $reflectionClass, Schema $schema)
{
/** @var SwgDefinition $swgDefinition */
if (!$swgDefinition = $this->annotationsReader->getClassAnnotation($reflectionClass, SwgDefinition::class)) {
return;
}
// Read @Model annotations
$this->modelRegister->__invoke(new Analysis([$swgDefinition]));
if (!$swgDefinition->validate()) {
return;
}
$schema->merge(json_decode(json_encode($swgDefinition)));
}
public function getPropertyName(\ReflectionProperty $reflectionProperty, string $default): string
{
/** @var SwgProperty $swgProperty */
if (!$swgProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, SwgProperty::class)) {
return $default;
}
return $swgProperty->property ?? $default;
}
public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property, array $serializationGroups = null)
{
if (!$swgProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, SwgProperty::class)) {
return;
}
$declaringClass = $reflectionProperty->getDeclaringClass();
$context = new Context([
'namespace' => $declaringClass->getNamespaceName(),
'class' => $declaringClass->getShortName(),
'property' => $reflectionProperty->name,
'filename' => $declaringClass->getFileName(),
]);
$swgProperty->_context = $context;
// Read @Model annotations
$this->modelRegister->__invoke(new Analysis([$swgProperty]), $serializationGroups);
if (!$swgProperty->validate()) {
return;
}
$property->merge(json_decode(json_encode($swgProperty)));
}
}

View File

@ -12,7 +12,7 @@
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations; namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Schema; use OpenApi\Annotations as OA;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@ -26,7 +26,7 @@ class SymfonyConstraintAnnotationReader
private $annotationsReader; private $annotationsReader;
/** /**
* @var Schema * @var OA\Schema
*/ */
private $schema; private $schema;
@ -38,7 +38,7 @@ class SymfonyConstraintAnnotationReader
/** /**
* Update the given property and schema with defined Symfony constraints. * Update the given property and schema with defined Symfony constraints.
*/ */
public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property) public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property): void
{ {
$annotations = $this->annotationsReader->getPropertyAnnotations($reflectionProperty); $annotations = $this->annotationsReader->getPropertyAnnotations($reflectionProperty);
@ -54,35 +54,35 @@ class SymfonyConstraintAnnotationReader
continue; continue;
} }
$existingRequiredFields = $this->schema->getRequired() ?? []; $existingRequiredFields = OA\UNDEFINED !== $this->schema->required ? $this->schema->required : [];
$existingRequiredFields[] = $propertyName; $existingRequiredFields[] = $propertyName;
$this->schema->setRequired(array_values(array_unique($existingRequiredFields))); $this->schema->required = array_values(array_unique($existingRequiredFields));
} elseif ($annotation instanceof Assert\Length) { } elseif ($annotation instanceof Assert\Length) {
$property->setMinLength($annotation->min); $property->minLength = $annotation->min;
$property->setMaxLength($annotation->max); $property->maxLength = $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->setMinItems($annotation->min); $property->minItems = $annotation->min;
$property->setMaxItems($annotation->max); $property->maxItems = $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->setEnum(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->setMinimum($annotation->min); $property->minimum = $annotation->min;
$property->setMaximum($annotation->max); $property->maximum = $annotation->max;
} elseif ($annotation instanceof Assert\LessThan) { } elseif ($annotation instanceof Assert\LessThan) {
$property->setExclusiveMaximum($annotation->value); $property->exclusiveMaximum= $annotation->value;
} elseif ($annotation instanceof Assert\LessThanOrEqual) { } elseif ($annotation instanceof Assert\LessThanOrEqual) {
$property->setMaximum($annotation->value); $property->maximum = $annotation->value;
} }
} }
} }
public function setSchema($schema) public function setSchema($schema): void
{ {
$this->schema = $schema; $this->schema = $schema;
} }
@ -90,15 +90,14 @@ class SymfonyConstraintAnnotationReader
/** /**
* Get assigned property name for property schema. * Get assigned property name for property schema.
*/ */
private function getSchemaPropertyName(Schema $property) private function getSchemaPropertyName(OA\Schema $property): ?string
{ {
if (null === $this->schema) { if (null === $this->schema) {
return null; return null;
} }
foreach ($this->schema->properties as $schemaProperty) {
foreach ($this->schema->getProperties() as $name => $schemaProperty) {
if ($schemaProperty === $property) { if ($schemaProperty === $property) {
return $name; return OA\UNDEFINED !== $schemaProperty->property ? $schemaProperty->property : null;
} }
} }
@ -108,16 +107,15 @@ class SymfonyConstraintAnnotationReader
/** /**
* Append the pattern from the constraint to the existing pattern. * Append the pattern from the constraint to the existing pattern.
*/ */
private function appendPattern(Schema $property, $newPattern) private function appendPattern(OA\Schema $property, $newPattern): void
{ {
if (null === $newPattern) { if (null === $newPattern) {
return; return;
} }
if (OA\UNDEFINED !== $property->pattern) {
if (null !== $property->getPattern()) { $property->pattern = sprintf('%s, %s', $property->pattern, $newPattern);
$property->setPattern(sprintf('%s, %s', $property->getPattern(), $newPattern));
} else { } else {
$property->setPattern($newPattern); $property->pattern = $newPattern;
} }
} }
} }

View File

@ -11,7 +11,6 @@
namespace Nelmio\ApiDocBundle\ModelDescriber; namespace Nelmio\ApiDocBundle\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Hateoas\Configuration\Metadata\ClassMetadata; use Hateoas\Configuration\Metadata\ClassMetadata;
use Hateoas\Configuration\Relation; use Hateoas\Configuration\Relation;
use Hateoas\Serializer\Metadata\RelationPropertyMetadata; use Hateoas\Serializer\Metadata\RelationPropertyMetadata;
@ -20,6 +19,8 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
{ {
@ -43,7 +44,7 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function describe(Model $model, Schema $schema) public function describe(Model $model, OA\Schema $schema): void
{ {
$this->JMSModelDescriber->describe($model, $schema); $this->JMSModelDescriber->describe($model, $schema);
@ -55,9 +56,10 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi
return; return;
} }
$schema->setType('object'); $schema->type = 'object';
$context = $this->JMSModelDescriber->getSerializationContext($model); $context = $this->JMSModelDescriber->getSerializationContext($model);
/** @var Relation $relation */
foreach ($metadata->getRelations() as $relation) { foreach ($metadata->getRelations() as $relation) {
if (!$relation->getEmbedded() && !$relation->getHref()) { if (!$relation->getEmbedded() && !$relation->getHref()) {
continue; continue;
@ -71,26 +73,19 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi
$context->pushPropertyMetadata($item); $context->pushPropertyMetadata($item);
$embedded = $relation->getEmbedded(); $embedded = $relation->getEmbedded();
$relationSchema = $schema->getProperties()->get($embedded ? '_embedded' : '_links'); $relationSchema = Util::getProperty($schema, $relation->getEmbedded() ? '_embedded' : '_links');
$relationSchema->readOnly = true;
$properties = $relationSchema->getProperties();
$relationSchema->setReadOnly(true);
$name = $relation->getName();
$property = $properties->get($name);
$property = Util::getProperty($relationSchema, $relation->getName());
if ($embedded && method_exists($embedded, 'getType') && $embedded->getType()) { if ($embedded && method_exists($embedded, 'getType') && $embedded->getType()) {
$this->JMSModelDescriber->describeItem($embedded->getType(), $property, $context); $this->JMSModelDescriber->describeItem($embedded->getType(), $property, $context);
} else { } else {
$property->setType('object'); $property->type = 'object';
} }
if ($relation->getHref()) { if ($relation->getHref()) {
$subProperties = $property->getProperties(); $hrefProp = Util::getProperty($property, 'href');
$hrefProp->type = 'string';
$hrefProp = $subProperties->get('href'); $this->setAttributeProperties($relation, $property);
$hrefProp->setType('string');
$this->setAttributeProperties($relation, $subProperties);
} }
$context->popPropertyMetadata(); $context->popPropertyMetadata();
@ -119,30 +114,30 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi
return $this->JMSModelDescriber->supports($model) || null !== $this->getHateoasMetadata($model); return $this->JMSModelDescriber->supports($model) || null !== $this->getHateoasMetadata($model);
} }
private function setAttributeProperties(Relation $relation, $subProperties) private function setAttributeProperties(Relation $relation, OA\Property $subProperty): void
{ {
foreach ($relation->getAttributes() as $attribute => $value) { foreach ($relation->getAttributes() as $attribute => $value) {
$subSubProp = $subProperties->get($attribute); $subSubProp = Util::getProperty($subProperty, $attribute);
switch (gettype($value)) { switch (gettype($value)) {
case 'integer': case 'integer':
$subSubProp->setType('integer'); $subSubProp->type = 'integer';
$subSubProp->setDefault($value); $subSubProp->default = $value;
break; break;
case 'double': case 'double':
case 'float': case 'float':
$subSubProp->setType('number'); $subSubProp->type = 'number';
$subSubProp->setDefault($value); $subSubProp->default = $value;
break; break;
case 'boolean': case 'boolean':
$subSubProp->setType('boolean'); $subSubProp->type = 'boolean';
$subSubProp->setDefault($value); $subSubProp->default = $value;
break; break;
case 'string': case 'string':
$subSubProp->setType('string'); $subSubProp->type = 'string';
$subSubProp->setDefault($value); $subSubProp->default = $value;
break; break;
} }

View File

@ -11,13 +11,13 @@
namespace Nelmio\ApiDocBundle\ModelDescriber; namespace Nelmio\ApiDocBundle\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\Model;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
class FallbackObjectModelDescriber implements ModelDescriberInterface class FallbackObjectModelDescriber implements ModelDescriberInterface
{ {
public function describe(Model $model, Schema $schema) public function describe(Model $model, OA\Schema $schema)
{ {
} }

View File

@ -11,13 +11,14 @@
namespace Nelmio\ApiDocBundle\ModelDescriber; namespace Nelmio\ApiDocBundle\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormConfigBuilderInterface; use Symfony\Component\Form\FormConfigInterface;
use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormTypeInterface; use Symfony\Component\Form\FormTypeInterface;
@ -38,7 +39,7 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
$this->formFactory = $formFactory; $this->formFactory = $formFactory;
} }
public function describe(Model $model, Schema $schema) public function describe(Model $model, OA\Schema $schema)
{ {
if (method_exists(AbstractType::class, 'setDefaultOptions')) { if (method_exists(AbstractType::class, 'setDefaultOptions')) {
throw new \LogicException('symfony/form < 3.0 is not supported, please upgrade to an higher version to use a form as a model.'); throw new \LogicException('symfony/form < 3.0 is not supported, please upgrade to an higher version to use a form as a model.');
@ -47,7 +48,7 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
throw new \LogicException('You need to enable forms in your application to use a form as a model.'); throw new \LogicException('You need to enable forms in your application to use a form as a model.');
} }
$schema->setType('object'); $schema->type = 'object';
$class = $model->getType()->getClassName(); $class = $model->getType()->getClassName();
@ -60,26 +61,24 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
return is_a($model->getType()->getClassName(), FormTypeInterface::class, true); return is_a($model->getType()->getClassName(), FormTypeInterface::class, true);
} }
private function parseForm(Schema $schema, FormInterface $form) private function parseForm(OA\Schema $schema, FormInterface $form)
{ {
$properties = $schema->getProperties();
foreach ($form as $name => $child) { foreach ($form as $name => $child) {
$config = $child->getConfig(); $config = $child->getConfig();
$property = $properties->get($name); $property = Util::getProperty($schema, $name);
if ($config->getRequired()) { if ($config->getRequired()) {
$required = $schema->getRequired() ?? []; $required = OA\UNDEFINED !== $schema->required ? $schema->required : [];
$required[] = $name; $required[] = $name;
$schema->setRequired($required); $schema->required = $required;
} }
if ($config->hasOption('documentation')) { if ($config->hasOption('documentation')) {
$property->merge($config->getOption('documentation')); $property->mergeProperties($config->getOption('documentation'));
} }
if (null !== $property->getType()) { if (OA\UNDEFINED !== $property->type) {
continue; // Type manually defined continue; // Type manually defined
} }
@ -90,12 +89,9 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
/** /**
* Finds and sets the schema type on $property based on $config info. * Finds and sets the schema type on $property based on $config info.
* *
* Returns true if a native Swagger type was found, false otherwise * Returns true if a native OpenAPi type was found, false otherwise
*
* @param FormConfigBuilderInterface $config
* @param $property
*/ */
private function findFormType(FormConfigBuilderInterface $config, $property) private function findFormType(FormConfigInterface $config, OA\Schema $property)
{ {
$type = $config->getType(); $type = $config->getType();
@ -106,7 +102,7 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
null, null,
$config->getOptions() $config->getOptions()
); );
$property->setRef($this->modelRegistry->register($model)); $property->ref = $this->modelRegistry->register($model);
return; return;
} }
@ -115,42 +111,42 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
$blockPrefix = $builtinFormType->getBlockPrefix(); $blockPrefix = $builtinFormType->getBlockPrefix();
if ('text' === $blockPrefix) { if ('text' === $blockPrefix) {
$property->setType('string'); $property->type = 'string';
break; break;
} }
if ('number' === $blockPrefix) { if ('number' === $blockPrefix) {
$property->setType('number'); $property->type = 'number';
break; break;
} }
if ('integer' === $blockPrefix) { if ('integer' === $blockPrefix) {
$property->setType('integer'); $property->type = 'integer';
break; break;
} }
if ('date' === $blockPrefix) { if ('date' === $blockPrefix) {
$property->setType('string'); $property->type = 'string';
$property->setFormat('date'); $property->format = 'date';
break; break;
} }
if ('datetime' === $blockPrefix) { if ('datetime' === $blockPrefix) {
$property->setType('string'); $property->type = 'string';
$property->setFormat('date-time'); $property->format = 'date-time';
break; break;
} }
if ('choice' === $blockPrefix) { if ('choice' === $blockPrefix) {
if ($config->getOption('multiple')) { if ($config->getOption('multiple')) {
$property->setType('array'); $property->type = 'array';
} else { } else {
$property->setType('string'); $property->type = 'string';
} }
if (($choices = $config->getOption('choices')) && is_array($choices) && count($choices)) { if (($choices = $config->getOption('choices')) && is_array($choices) && count($choices)) {
$enums = array_values($choices); $enums = array_values($choices);
@ -163,9 +159,10 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
} }
if ($config->getOption('multiple')) { if ($config->getOption('multiple')) {
$property->getItems()->setType($type)->setEnum($enums); $property->items = Util::createChild($property, OA\Items::class, ['type' => $type, 'enum' => $enums]);
} else { } else {
$property->setType($type)->setEnum($enums); $property->type = $type;
$property->enum = $enums;
} }
} }
@ -173,28 +170,28 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
} }
if ('checkbox' === $blockPrefix) { if ('checkbox' === $blockPrefix) {
$property->setType('boolean'); $property->type= 'boolean';
break; break;
} }
if ('password' === $blockPrefix) { if ('password' === $blockPrefix) {
$property->setType('string'); $property->type = 'string';
$property->setFormat('password'); $property->format = 'password';
break; break;
} }
if ('repeated' === $blockPrefix) { if ('repeated' === $blockPrefix) {
$property->setType('object'); $property->type = 'object';
$property->setRequired([$config->getOption('first_name'), $config->getOption('second_name')]); $property->required = [$config->getOption('first_name'), $config->getOption('second_name')];
$subType = $config->getOption('type'); $subType = $config->getOption('type');
foreach (['first', 'second'] as $subField) { foreach (['first', 'second'] as $subField) {
$subName = $config->getOption($subField.'_name'); $subName = $config->getOption($subField.'_name');
$subForm = $this->formFactory->create($subType, null, array_merge($config->getOption('options'), $config->getOption($subField.'_options'))); $subForm = $this->formFactory->create($subType, null, array_merge($config->getOption('options'), $config->getOption($subField.'_options')));
$this->findFormType($subForm->getConfig(), $property->getProperties()->get($subName)); $this->findFormType($subForm->getConfig(), Util::getProperty($property, $subName));
} }
break; break;
@ -205,10 +202,10 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
$subOptions = $config->getOption('entry_options'); $subOptions = $config->getOption('entry_options');
$subForm = $this->formFactory->create($subType, null, $subOptions); $subForm = $this->formFactory->create($subType, null, $subOptions);
$property->setType('array'); $property->type = 'array';
$itemsProp = $property->getItems(); $property->items = Util::createChild($property, OA\Items::class);
$this->findFormType($subForm->getConfig(), $itemsProp); $this->findFormType($subForm->getConfig(), $property->items);
break; break;
} }
@ -218,12 +215,12 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
$entityClass = $config->getOption('class'); $entityClass = $config->getOption('class');
if ($config->getOption('multiple')) { if ($config->getOption('multiple')) {
$property->setFormat(sprintf('[%s id]', $entityClass)); $property->format = sprintf('[%s id]', $entityClass);
$property->setType('array'); $property->type = 'array';
$property->getItems()->setType('string'); $property->items = Util::createChild($property, OA\Items::class, ['type' => 'string']);
} else { } else {
$property->setType('string'); $property->type = 'string';
$property->setFormat(sprintf('%s id', $entityClass)); $property->format = sprintf('%s id', $entityClass);
} }
break; break;

View File

@ -12,7 +12,6 @@
namespace Nelmio\ApiDocBundle\ModelDescriber; namespace Nelmio\ApiDocBundle\ModelDescriber;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Schema;
use JMS\Serializer\Context; use JMS\Serializer\Context;
use JMS\Serializer\Exclusion\GroupsExclusionStrategy; use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface; use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
@ -22,6 +21,8 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader; use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
/** /**
@ -41,6 +42,8 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
private $metadataStacks = []; private $metadataStacks = [];
private $mediaTypes;
/** /**
* @var array * @var array
*/ */
@ -48,18 +51,20 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
public function __construct( public function __construct(
MetadataFactoryInterface $factory, MetadataFactoryInterface $factory,
PropertyNamingStrategyInterface $namingStrategy = null, Reader $reader,
Reader $reader array $mediaTypes,
?PropertyNamingStrategyInterface $namingStrategy = null
) { ) {
$this->factory = $factory; $this->factory = $factory;
$this->namingStrategy = $namingStrategy; $this->namingStrategy = $namingStrategy;
$this->doctrineReader = $reader; $this->doctrineReader = $reader;
$this->mediaTypes = $mediaTypes;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function describe(Model $model, Schema $schema) public function describe(Model $model, OA\Schema $schema)
{ {
$className = $model->getType()->getClassName(); $className = $model->getType()->getClassName();
$metadata = $this->factory->getMetadataForClass($className); $metadata = $this->factory->getMetadataForClass($className);
@ -67,12 +72,11 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
throw new \InvalidArgumentException(sprintf('No metadata found for class %s.', $className)); throw new \InvalidArgumentException(sprintf('No metadata found for class %s.', $className));
} }
$schema->setType('object'); $schema->type = 'object';
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry); $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
$annotationsReader->updateDefinition(new \ReflectionClass($className), $schema); $annotationsReader->updateDefinition(new \ReflectionClass($className), $schema);
$isJmsV1 = null !== $this->namingStrategy; $isJmsV1 = null !== $this->namingStrategy;
$properties = $schema->getProperties();
$context = $this->getSerializationContext($model); $context = $this->getSerializationContext($model);
$context->pushClassMetadata($metadata); $context->pushClassMetadata($metadata);
@ -106,25 +110,26 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
continue; continue;
} }
$property = $properties->get($annotationsReader->getPropertyName($reflection, $name)); $property = Util::getProperty($schema, $annotationsReader->getPropertyName($reflection, $name));
$annotationsReader->updateProperty($reflection, $property, $groups); $annotationsReader->updateProperty($reflection, $property, $groups);
} catch (\ReflectionException $e) { } catch (\ReflectionException $e) {
$property = $properties->get($name); $property = Util::getProperty($schema, $name);
} }
if (null !== $property->getType() || null !== $property->getRef()) { if (OA\UNDEFINED !== $property->type || OA\UNDEFINED !== $property->ref) {
$context->popPropertyMetadata(); $context->popPropertyMetadata();
continue; continue;
} }
if (null === $item->type) { if (null === $item->type) {
$properties->remove($name); $key = Util::searchIndexedCollectionItem($schema->properties, 'property', $name);
unset($schema->properties[$key]);
$context->popPropertyMetadata(); $context->popPropertyMetadata();
continue; continue;
} }
$this->describeItem($item->type, $property, $context, $item); $this->describeItem($item->type, $property, $context);
$context->popPropertyMetadata(); $context->popPropertyMetadata();
} }
$context->popClassMetadata(); $context->popClassMetadata();
@ -196,48 +201,51 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
/** /**
* @internal * @internal
*/ */
public function describeItem(array $type, $property, Context $context) public function describeItem(array $type, OA\Schema $property, Context $context)
{ {
$nestedTypeInfo = $this->getNestedTypeInArray($type); $nestedTypeInfo = $this->getNestedTypeInArray($type);
if (null !== $nestedTypeInfo) { if (null !== $nestedTypeInfo) {
list($nestedType, $isHash) = $nestedTypeInfo; list($nestedType, $isHash) = $nestedTypeInfo;
if ($isHash) { if ($isHash) {
$property->setType('object'); $property->type = 'object';
// in the case of a virtual property, set it as free object type $property->additionalProperties = Util::createChild($property, OA\Property::class);
$property->merge(['additionalProperties' => []]);
// this is a free form object (as nested array) // this is a free form object (as nested array)
if ('array' === $nestedType['name'] && !isset($nestedType['params'][0])) { if ('array' === $nestedType['name'] && !isset($nestedType['params'][0])) {
// in the case of a virtual property, set it as free object type
$property->additionalProperties = true;
return; return;
} }
$this->describeItem($nestedType, $property->getAdditionalProperties(), $context); $this->describeItem($nestedType, $property->additionalProperties, $context);
return; return;
} }
$property->setType('array'); $property->type = 'array';
$this->describeItem($nestedType, $property->getItems(), $context); $property->items = Util::createChild($property, OA\Items::class);
$this->describeItem($nestedType, $property->items, $context);
} elseif ('array' === $type['name']) { } elseif ('array' === $type['name']) {
$property->setType('object'); $property->type = 'object';
$property->merge(['additionalProperties' => []]); $property->additionalProperties = true;
} elseif ('string' === $type['name']) { } elseif ('string' === $type['name']) {
$property->setType('string'); $property->type = 'string';
} elseif (in_array($type['name'], ['bool', 'boolean'], true)) { } elseif (in_array($type['name'], ['bool', 'boolean'], true)) {
$property->setType('boolean'); $property->type = 'boolean';
} elseif (in_array($type['name'], ['int', 'integer'], true)) { } elseif (in_array($type['name'], ['int', 'integer'], true)) {
$property->setType('integer'); $property->type = 'integer';
} elseif (in_array($type['name'], ['double', 'float'], true)) { } elseif (in_array($type['name'], ['double', 'float'], true)) {
$property->setType('number'); $property->type = 'number';
$property->setFormat($type['name']); $property->format = $type['name'];
} elseif (is_subclass_of($type['name'], \DateTimeInterface::class)) { } elseif (is_subclass_of($type['name'], \DateTimeInterface::class)) {
$property->setType('string'); $property->type = 'string';
$property->setFormat('date-time'); $property->format = 'date-time';
} else { } else {
$groups = $this->computeGroups($context, $type); $groups = $this->computeGroups($context, $type);
$model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type['name']), $groups); $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type['name']), $groups);
$property->setRef($this->modelRegistry->register($model)); $property->ref = $this->modelRegistry->register($model);
$this->contexts[$model->getHash()] = $context; $this->contexts[$model->getHash()] = $context;
$this->metadataStacks[$model->getHash()] = clone $context->getMetadataStack(); $this->metadataStacks[$model->getHash()] = clone $context->getMetadataStack();

View File

@ -11,8 +11,8 @@
namespace Nelmio\ApiDocBundle\ModelDescriber; namespace Nelmio\ApiDocBundle\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\Model;
use OpenApi\Annotations\Schema;
interface ModelDescriberInterface interface ModelDescriberInterface
{ {

View File

@ -12,12 +12,13 @@
namespace Nelmio\ApiDocBundle\ModelDescriber; namespace Nelmio\ApiDocBundle\ModelDescriber;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader; use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface; use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
@ -31,31 +32,36 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
private $doctrineReader; private $doctrineReader;
/** @var PropertyDescriberInterface[] */ /** @var PropertyDescriberInterface[] */
private $propertyDescribers; private $propertyDescribers;
/** @var string[] */
private $mediaTypes;
private $swaggerDefinitionAnnotationReader; private $swaggerDefinitionAnnotationReader;
public function __construct( public function __construct(
PropertyInfoExtractorInterface $propertyInfo, PropertyInfoExtractorInterface $propertyInfo,
Reader $reader, Reader $reader,
$propertyDescribers $propertyDescribers,
array $mediaTypes
) { ) {
$this->propertyInfo = $propertyInfo; $this->propertyInfo = $propertyInfo;
$this->doctrineReader = $reader; $this->doctrineReader = $reader;
$this->propertyDescribers = $propertyDescribers; $this->propertyDescribers = $propertyDescribers;
$this->mediaTypes = $mediaTypes;
} }
public function describe(Model $model, Schema $schema) public function describe(Model $model, OA\Schema $schema)
{ {
$schema->setType('object'); $schema->type = 'object';
$properties = $schema->getProperties();
$class = $model->getType()->getClassName(); $class = $model->getType()->getClassName();
$schema->_context->class = $class;
$context = []; $context = [];
if (null !== $model->getGroups()) { if (null !== $model->getGroups()) {
$context = ['serializer_groups' => array_filter($model->getGroups(), 'is_string')]; $context = ['serializer_groups' => array_filter($model->getGroups(), 'is_string')];
} }
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry); $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
$annotationsReader->updateDefinition(new \ReflectionClass($class), $schema); $annotationsReader->updateDefinition(new \ReflectionClass($class), $schema);
$propertyInfoProperties = $this->propertyInfo->getProperties($class, $context); $propertyInfoProperties = $this->propertyInfo->getProperties($class, $context);
@ -64,10 +70,10 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
} }
foreach ($propertyInfoProperties as $propertyName) { foreach ($propertyInfoProperties as $propertyName) {
// read property options from Swagger Property annotation if it exists // read property options from OpenApi Property annotation if it exists
if (property_exists($class, $propertyName)) { if (property_exists($class, $propertyName)) {
$reflectionProperty = new \ReflectionProperty($class, $propertyName); $reflectionProperty = new \ReflectionProperty($class, $propertyName);
$property = $properties->get($annotationsReader->getPropertyName($reflectionProperty, $propertyName)); $property = Util::getProperty($schema, $annotationsReader->getPropertyName($reflectionProperty, $propertyName));
$groups = $model->getGroups(); $groups = $model->getGroups();
if (isset($groups[$propertyName]) && is_array($groups[$propertyName])) { if (isset($groups[$propertyName]) && is_array($groups[$propertyName])) {
@ -76,11 +82,11 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
$annotationsReader->updateProperty($reflectionProperty, $property, $groups); $annotationsReader->updateProperty($reflectionProperty, $property, $groups);
} else { } else {
$property = $properties->get($propertyName); $property = Util::getProperty($schema, $propertyName);
} }
// If type manually defined // If type manually defined
if (null !== $property->getType() || null !== $property->getRef()) { if (OA\UNDEFINED !== $property->type || OA\UNDEFINED !== $property->ref) {
continue; continue;
} }
@ -97,7 +103,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
} }
} }
private function describeProperty(Type $type, Model $model, Schema $property, string $propertyName) private function describeProperty(Type $type, Model $model, OA\Schema $property, string $propertyName)
{ {
foreach ($this->propertyDescribers as $propertyDescriber) { foreach ($this->propertyDescribers as $propertyDescriber) {
if ($propertyDescriber instanceof ModelRegistryAwareInterface) { if ($propertyDescriber instanceof ModelRegistryAwareInterface) {
@ -110,7 +116,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
} }
} }
throw new \Exception(sprintf('Type "%s" is not supported in %s::$%s. You may use the `@SWG\Property(type="")` annotation to specify it manually.', $type->getBuiltinType(), $model->getType()->getClassName(), $propertyName)); throw new \Exception(sprintf('Type "%s" is not supported in %s::$%s. You may use the `@OA\Property(type="")` annotation to specify it manually.', $type->getBuiltinType(), $model->getType()->getClassName(), $propertyName));
} }
public function supports(Model $model): bool public function supports(Model $model): bool

View File

@ -0,0 +1,39 @@
<?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

@ -0,0 +1,171 @@
<?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 Nelmio\ApiDocBundle\Annotation\Model as ModelAnnotation;
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type;
/**
* Resolves the path in SwaggerPhp annotation when needed.
*
* @internal
*/
final class ModelRegister
{
/** @var ModelRegistry */
private $modelRegistry;
/** @var string[] */
private $mediaTypes;
public function __construct(ModelRegistry $modelRegistry, array $mediaTypes)
{
$this->modelRegistry = $modelRegistry;
$this->mediaTypes = $mediaTypes;
}
public function __invoke(Analysis $analysis, array $parentGroups = null)
{
foreach ($analysis->annotations as $annotation) {
// @Model using the ref field
if ($annotation instanceof OA\Schema && $annotation->ref instanceof ModelAnnotation) {
$model = $annotation->ref;
$annotation->ref = $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options));
// It is no longer an unmerged annotation
$this->detach($model, $annotation, $analysis);
continue;
}
// Misusage of ::$ref
if (($annotation instanceof OA\Response || $annotation instanceof OA\RequestBody) && $annotation->ref instanceof ModelAnnotation) {
throw new \InvalidArgumentException(sprintf('Using @Model inside @%s::$ref is not allowed. You should use ::$ref with @Property, @Parameter, @Schema, @Items but within @Response or @RequestBody you should put @Model directly at the root of the annotation : `@Response(..., @Model(...))`.', get_class($annotation)));
}
// Implicit usages
// We don't use $ref for @Responses, @RequestBody and @Parameter to respect semantics
// We don't replace these objects with the @Model found (we inject it in a subfield) whereas we do for @Schemas
$model = $this->getModel($annotation); // We check whether there is a @Model annotation nested
if (null === $model) {
continue;
}
if ($annotation instanceof OA\Response || $annotation instanceof OA\RequestBody) {
$properties = [
'_context' => Util::createContext(['nested' => $annotation], $annotation->_context),
'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options)),
];
foreach ($this->mediaTypes as $mediaType) {
$this->createContentForMediaType($mediaType, $properties, $annotation, $analysis);
}
$this->detach($model, $annotation, $analysis);
continue;
}
if (!$annotation instanceof OA\Parameter) {
throw new \InvalidArgumentException(sprintf("@Model annotation can't be nested with an annotation of type @%s.", get_class($annotation)));
}
if ($annotation->schema instanceof OA\Schema && 'array' === $annotation->schema->type) {
$annotationClass = OA\Items::class;
} else {
$annotationClass = OA\Schema::class;
}
if (!is_string($model->type)) {
// Ignore invalid annotations, they are validated later
continue;
}
$annotation->merge([new $annotationClass([
'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options)),
])]);
// It is no longer an unmerged annotation
$this->detach($model, $annotation, $analysis);
}
}
private function getGroups(ModelAnnotation $model, array $parentGroups = null)
{
if (null === $model->groups) {
return $parentGroups;
}
return array_merge($parentGroups ?? [], $model->groups);
}
private function detach(ModelAnnotation $model, OA\AbstractAnnotation $annotation, Analysis $analysis)
{
foreach ($annotation->_unmerged as $key => $unmerged) {
if ($unmerged === $model) {
unset($annotation->_unmerged[$key]);
break;
}
}
$analysis->annotations->detach($model);
}
private function createType(string $type): Type
{
if ('[]' === substr($type, -2)) {
return new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, $this->createType(substr($type, 0, -2)));
}
return new Type(Type::BUILTIN_TYPE_OBJECT, false, $type);
}
private function getModel(OA\AbstractAnnotation $annotation): ?ModelAnnotation
{
foreach ($annotation->_unmerged as $unmerged) {
if ($unmerged instanceof ModelAnnotation) {
return $unmerged;
}
}
return null;
}
private function createContentForMediaType(
string $type,
array $properties,
OA\AbstractAnnotation $annotation,
Analysis $analysis
) {
switch ($type) {
case 'json':
$modelAnnotation = new OA\JsonContent($properties);
break;
case 'xml':
$modelAnnotation = new OA\XmlContent($properties);
break;
default:
throw new \InvalidArgumentException(sprintf("@Model annotation is not compatible with the media type '%s'. It must be one of 'json' or 'xml'.", $this->mediaType));
}
$annotation->merge([$modelAnnotation]);
$analysis->addAnnotation($modelAnnotation, null);
}
}

519
OpenApiPhp/Util.php Normal file
View File

@ -0,0 +1,519 @@
<?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\Annotations as OA;
use OpenApi\Context;
use const OpenApi\UNDEFINED;
/**
* Class Util.
*
* This class acts as compatibility layer between NelmioApiDocBundle and swagger-php.
*
* It was written to replace the GuilhemN/swagger layer as a lower effort to maintain alternative.
*
* The main purpose of this class is to search for and create child Annotations
* of swagger Annotation classes with the following convenience methods
* to get or create the respective Annotation instances if not found
*
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getPath()
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getSchema()
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getProperty()
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getOperation()
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getOperationParameter()
*
* which in turn get or create the Annotation instances through the following more general methods
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getChild()
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getCollectionItem()
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getIndexedCollectionItem()
*
* which then searches for an existing Annotation through
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::searchCollectionItem()
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::searchIndexedCollectionItem()
*
* and if not found the Annotation creates it through
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::createCollectionItem()
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::createContext()
*
* The merge method @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::merge() has the main purpose to be able
* to merge properties from an deeply nested array of Annotation properties in the structure of a
* generated swagger json decoded array.
*/
final class Util
{
/**
* All http method verbs as known by swagger.
*
* @var array
*/
public const OPERATIONS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'];
/**
* Return an existing PathItem object from $api->paths[] having its member path set to $path.
* Create, add to $api->paths[] and return this new PathItem object and set the property if none found.
*
* @see OA\OpenApi::$paths
* @see OA\PathItem::path
*
* @param OA\OpenApi $api
* @param string $path
*
* @return OA\PathItem
*/
public static function getPath(OA\OpenApi $api, $path): OA\PathItem
{
return self::getIndexedCollectionItem($api, OA\PathItem::class, $path);
}
/**
* Return an existing Schema object from $api->components->schemas[] having its member schema set to $schema.
* Create, add to $api->components->schemas[] and return this new Schema object and set the property if none found.
*
* @param OA\OpenApi $api
* @param string $schema
*
* @return OA\Schema
*
* @see OA\Schema::$schema
* @see OA\Components::$schemas
*/
public static function getSchema(OA\OpenApi $api, $schema): OA\Schema
{
if (!$api->components instanceof OA\Components) {
$api->components = new OA\Components([]);
}
return self::getIndexedCollectionItem($api->components, OA\Schema::class, $schema);
}
/**
* Return an existing Property object from $schema->properties[]
* having its member property set to $property.
*
* Create, add to $schema->properties[] and return this new Property object
* and set the property if none found.
*
* @see OA\Schema::$properties
* @see OA\Property::$property
*
* @param OA\Schema $schema
* @param string $property
*
* @return OA\Property
*/
public static function getProperty(OA\Schema $schema, $property): OA\Property
{
return self::getIndexedCollectionItem($schema, OA\Property::class, $property);
}
/**
* Return an existing Operation from $path->{$method}
* or create, set $path->{$method} and return this new Operation object.
*
* @see OA\PathItem::$get
* @see OA\PathItem::$post
* @see OA\PathItem::$put
* @see OA\PathItem::$patch
* @see OA\PathItem::$delete
* @see OA\PathItem::$options
* @see OA\PathItem::$head
*
* @param OA\PathItem $path
* @param string $method
*
* @return OA\Operation
*/
public static function getOperation(OA\PathItem $path, $method): OA\Operation
{
$class = array_keys($path::$_nested, \strtolower($method), true)[0];
return self::getChild($path, $class, ['path' => $path->path]);
}
/**
* Return an existing Parameter object from $operation->parameters[]
* having its members name set to $name and in set to $in.
*
* Create, add to $operation->parameters[] and return
* this new Parameter object and set its members if none found.
*
* @see OA\Operation::$parameters
* @see OA\Parameter::$name
* @see OA\Parameter::$in
*
* @param OA\Operation $operation
* @param string $name
* @param string $in
*
* @return OA\Parameter
*/
public static function getOperationParameter(OA\Operation $operation, $name, $in): OA\Parameter
{
return self::getCollectionItem($operation, OA\Parameter::class, ['name' => $name, 'in' => $in]);
}
/**
* Return an existing nested Annotation from $parent->{$property} if exists.
* Create, add to $parent->{$property} and set its members to $properties otherwise.
*
* $property is determined from $parent::$_nested[$class]
* it is expected to be a string nested property.
*
* @see OA\AbstractAnnotation::$_nested
*
* @param OA\AbstractAnnotation $parent
* @param $class
* @param array $properties
*
* @return OA\AbstractAnnotation
*/
public static function getChild(OA\AbstractAnnotation $parent, $class, array $properties = []): OA\AbstractAnnotation
{
$nested = $parent::$_nested;
$property = $nested[$class];
if (null === $parent->{$property} || UNDEFINED === $parent->{$property}) {
$parent->{$property} = self::createChild($parent, $class, $properties);
}
return $parent->{$property};
}
/**
* Return an existing nested Annotation from $parent->{$collection}[]
* having all $properties set to the respective values.
*
* Create, add to $parent->{$collection}[] and set its members
* to $properties otherwise.
*
* $collection is determined from $parent::$_nested[$class]
* it is expected to be a single value array nested Annotation.
*
* @see OA\AbstractAnnotation::$_nested
*
* @param OA\AbstractAnnotation $parent
* @param string $class
* @param array $properties
*
* @return OA\AbstractAnnotation
*/
public static function getCollectionItem(OA\AbstractAnnotation $parent, $class, array $properties = []): OA\AbstractAnnotation
{
$key = null;
$nested = $parent::$_nested;
$collection = $nested[$class][0];
if (!empty($properties)) {
$key = self::searchCollectionItem(
$parent->{$collection} && UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [],
$properties
);
}
if (null === $key) {
$key = self::createCollectionItem($parent, $collection, $class, $properties);
}
return $parent->{$collection}[$key];
}
/**
* Return an existing nested Annotation from $parent->{$collection}[]
* having its mapped $property set to $value.
*
* Create, add to $parent->{$collection}[] and set its member $property to $value otherwise.
*
* $collection is determined from $parent::$_nested[$class]
* it is expected to be a double value array nested Annotation
* with the second value being the mapping index $property.
*
* @see OA\AbstractAnnotation::$_nested
*
* @param OA\AbstractAnnotation $parent
* @param string $class
* @param mixed $value
*
* @return OA\AbstractAnnotation
*/
public static function getIndexedCollectionItem(OA\AbstractAnnotation $parent, $class, $value): OA\AbstractAnnotation
{
$nested = $parent::$_nested;
[$collection, $property] = $nested[$class];
$key = self::searchIndexedCollectionItem(
$parent->{$collection} && UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [],
$property,
$value
);
if (false === $key) {
$key = self::createCollectionItem($parent, $collection, $class, [$property => $value]);
}
return $parent->{$collection}[$key];
}
/**
* Search for an Annotation within $collection that has all members set
* to the respective values in the associative array $properties.
*
* @param array $collection
* @param array $properties
*
* @return int|string|null
*/
public static function searchCollectionItem(array $collection, array $properties)
{
foreach ($collection ?: [] as $i => $child) {
foreach ($properties as $k => $prop) {
if ($child->{$k} !== $prop) {
continue 2;
}
}
return $i;
}
return null;
}
/**
* Search for an Annotation within the $collection that has its member $index set to $value.
*
* @param array $collection
* @param string $member
* @param mixed $value
*
* @return false|int|string
*/
public static function searchIndexedCollectionItem(array $collection, $member, $value)
{
return array_search($value, array_column($collection, $member), true);
}
/**
* Create a new Object of $class with members $properties within $parent->{$collection}[]
* and return the created index.
*
* @param OA\AbstractAnnotation $parent
* @param string $collection
* @param string $class
* @param array $properties
*
* @return int
*/
public static function createCollectionItem(OA\AbstractAnnotation $parent, $collection, $class, array $properties = []): int
{
if (UNDEFINED === $parent->{$collection}) {
$parent->{$collection} = [];
}
$key = \count($parent->{$collection} ?: []);
$parent->{$collection}[$key] = self::createChild($parent, $class, $properties);
return $key;
}
/**
* Create a new Object of $class with members $properties and set the context parent to be $parent.
*
*
* @param OA\AbstractAnnotation $parent
* @param string $class
* @param array $properties
*
* @throws \InvalidArgumentException at an attempt to pass in properties that are found in $parent::$_nested
*
* @return OA\AbstractAnnotation
*/
public static function createChild(OA\AbstractAnnotation $parent, $class, array $properties = []): OA\AbstractAnnotation
{
$nesting = self::getNestingIndexes($class);
if (!empty(array_intersect(array_keys($properties), $nesting))) {
throw new \InvalidArgumentException('Nesting Annotations is not supported.');
}
return new $class(
array_merge($properties, ['_context' => self::createContext(['nested' => $parent], $parent->_context)])
);
}
/**
* Create a new Context with members $properties and parent context $parent.
*
* @see Context
*
* @param array $properties
* @param Context|null $parent
*
* @return Context
*/
public static function createContext(array $properties = [], Context $parent = null): Context
{
return new Context($properties, $parent);
}
/**
* Merge $from into $annotation. $overwrite is only used for leaf scalar values.
*
* The main purpose is to create a Swagger Object from array config values
* in the structure of a json serialized Swagger object.
*
* @param OA\AbstractAnnotation $annotation
* @param array|\ArrayObject|OA\AbstractAnnotation $from
* @param bool $overwrite
*/
public static function merge(OA\AbstractAnnotation $annotation, $from, bool $overwrite = false)
{
if (\is_array($from)) {
self::mergeFromArray($annotation, $from, $overwrite);
} elseif (\is_a($from, OA\AbstractAnnotation::class)) {
/* @var OA\AbstractAnnotation $from */
self::mergeFromArray($annotation, json_decode(json_encode($from), true), $overwrite);
} elseif (\is_a($from, \ArrayObject::class)) {
/* @var \ArrayObject $from */
self::mergeFromArray($annotation, $from->getArrayCopy(), $overwrite);
}
}
private static function mergeFromArray(OA\AbstractAnnotation $annotation, array $properties, bool $overwrite)
{
$done = [];
foreach ($annotation::$_nested as $className => $propertyName) {
if (\is_string($propertyName)) {
if (array_key_exists($propertyName, $properties)) {
self::mergeChild($annotation, $className, $properties[$propertyName], $overwrite);
$done[] = $propertyName;
}
} elseif (\array_key_exists($propertyName[0], $properties)) {
$collection = $propertyName[0];
$property = $propertyName[1] ?? null;
self::mergeCollection($annotation, $className, $collection, $property, $properties[$collection], $overwrite);
$done[] = $collection;
}
}
$defaults = \get_class_vars(\get_class($annotation));
foreach ($annotation::$_types as $propertyName => $type) {
if (array_key_exists($propertyName, $properties)) {
self::mergeTyped($annotation, $propertyName, $type, $properties, $defaults, $overwrite);
$done[] = $propertyName;
}
}
foreach ($properties as $propertyName => $value) {
if ('$ref' === $propertyName) {
$propertyName = 'ref';
}
if (!\in_array($propertyName, $done, true)) {
self::mergeProperty($annotation, $propertyName, $value, $defaults[$propertyName], $overwrite);
}
}
}
private static function mergeChild(OA\AbstractAnnotation $annotation, $className, $value, bool $overwrite)
{
self::merge(self::getChild($annotation, $className), $value, $overwrite);
}
private static function mergeCollection(OA\AbstractAnnotation $annotation, $className, $collection, $property, $items, bool $overwrite)
{
if (null !== $property) {
foreach ($items as $prop => $value) {
$child = self::getIndexedCollectionItem($annotation, $className, (string) $prop);
self::merge($child, $value);
}
} else {
$nesting = self::getNestingIndexes($className);
foreach ($items as $props) {
$create = [];
$merge = [];
foreach ($props as $k => $v) {
if (\in_array($k, $nesting, true)) {
$merge[$k] = $v;
} else {
$create[$k] = $v;
}
}
self::merge(self::getCollectionItem($annotation, $className, $create), $merge, $overwrite);
}
}
}
private static function mergeTyped(OA\AbstractAnnotation $annotation, $propertyName, $type, array $properties, array $defaults, bool $overwrite)
{
if (\is_string($type) && 0 === strpos($type, '[')) {
/* type is declared as array in @see OA\AbstractAnnotation::$_types */
$annotation->{$propertyName} = array_unique(array_merge(
$annotation->{$propertyName} && UNDEFINED !== $annotation->{$propertyName} ? $annotation->{$propertyName} : [],
$properties[$propertyName]
));
} else {
self::mergeProperty($annotation, $propertyName, $properties[$propertyName], $defaults[$propertyName], $overwrite);
}
}
private static function mergeProperty(OA\AbstractAnnotation $annotation, $propertyName, $value, $default, bool $overwrite)
{
if (true === $overwrite || $default === $annotation->{$propertyName}) {
$annotation->{$propertyName} = $value;
}
}
private static function getNestingIndexes($class): array
{
return array_values(array_map(
function ($value) {
return \is_array($value) ? $value[0] : $value;
},
self::getNesting($class) ?? []
));
}
private static function getNesting($class)
{
switch ($class) {
case OA\OpenApi::class:
return OA\OpenApi::$_nested;
case OA\Info::class:
return OA\Info::$_nested;
case OA\PathItem::class:
return OA\PathItem::$_nested;
case OA\Get::class:
case OA\Post::class:
case OA\Put::class:
case OA\Delete::class:
case OA\Patch::class:
case OA\Head::class:
case OA\Options::class:
return OA\Operation::$_nested;
case OA\Parameter::class:
return OA\Parameter::$_nested;
case OA\Items::class:
return OA\Items::$_nested;
case OA\Property::class:
case OA\Schema::class:
return OA\Schema::$_nested;
case OA\Tag::class:
return OA\Tag::$_nested;
case OA\Response::class:
return OA\Response::$_nested;
case OA\Header::class:
return OA\Header::$_nested;
default:
return null;
}
}
}

View File

@ -11,9 +11,10 @@
namespace Nelmio\ApiDocBundle\PropertyDescriber; namespace Nelmio\ApiDocBundle\PropertyDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface
@ -28,15 +29,15 @@ class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistr
$this->propertyDescribers = $propertyDescribers; $this->propertyDescribers = $propertyDescribers;
} }
public function describe(Type $type, Schema $property, array $groups = null) public function describe(Type $type, OA\Schema $property, array $groups = null)
{ {
$type = $type->getCollectionValueType(); $type = $type->getCollectionValueType();
if (null === $type) { if (null === $type) {
throw new \LogicException(sprintf('Property "%s" is an array, but its items type isn\'t specified. You can specify that by using the type `string[]` for instance or `@SWG\Property(type="array", @SWG\Items(type="string"))`.', $property->getTitle())); throw new \LogicException(sprintf('Property "%s" is an array, but its items type isn\'t specified. You can specify that by using the type `string[]` for instance or `@SWG\Property(type="array", @SWG\Items(type="string"))`.', $property->getTitle()));
} }
$property->setType('array'); $property->type = 'array';
$property = $property->getItems(); $property = Util::getChild($property, OA\Items::class);
foreach ($this->propertyDescribers as $propertyDescriber) { foreach ($this->propertyDescribers as $propertyDescriber) {
if ($propertyDescriber instanceof ModelRegistryAwareInterface) { if ($propertyDescriber instanceof ModelRegistryAwareInterface) {

View File

@ -11,14 +11,14 @@
namespace Nelmio\ApiDocBundle\PropertyDescriber; namespace Nelmio\ApiDocBundle\PropertyDescriber;
use EXSyst\Component\Swagger\Schema; use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
class BooleanPropertyDescriber implements PropertyDescriberInterface class BooleanPropertyDescriber implements PropertyDescriberInterface
{ {
public function describe(Type $type, Schema $property, array $groups = null) public function describe(Type $type, OA\Schema $property, array $groups = null)
{ {
$property->setType('boolean'); $property->type = 'boolean';
} }
public function supports(Type $type): bool public function supports(Type $type): bool

View File

@ -11,15 +11,15 @@
namespace Nelmio\ApiDocBundle\PropertyDescriber; namespace Nelmio\ApiDocBundle\PropertyDescriber;
use EXSyst\Component\Swagger\Schema; use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
class DateTimePropertyDescriber implements PropertyDescriberInterface class DateTimePropertyDescriber implements PropertyDescriberInterface
{ {
public function describe(Type $type, Schema $property, array $groups = null) public function describe(Type $type, OA\Schema $property, array $groups = null)
{ {
$property->setType('string'); $property->type = 'string';
$property->setFormat('date-time'); $property->format = 'date-time';
} }
public function supports(Type $type): bool public function supports(Type $type): bool

View File

@ -11,15 +11,15 @@
namespace Nelmio\ApiDocBundle\PropertyDescriber; namespace Nelmio\ApiDocBundle\PropertyDescriber;
use EXSyst\Component\Swagger\Schema; use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
class FloatPropertyDescriber implements PropertyDescriberInterface class FloatPropertyDescriber implements PropertyDescriberInterface
{ {
public function describe(Type $type, Schema $property, array $groups = null) public function describe(Type $type, OA\Schema $property, array $groups = null)
{ {
$property->setType('number'); $property->type = 'number';
$property->setFormat('float'); $property->format = 'float';
} }
public function supports(Type $type): bool public function supports(Type $type): bool

View File

@ -11,14 +11,14 @@
namespace Nelmio\ApiDocBundle\PropertyDescriber; namespace Nelmio\ApiDocBundle\PropertyDescriber;
use EXSyst\Component\Swagger\Schema; use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
class IntegerPropertyDescriber implements PropertyDescriberInterface class IntegerPropertyDescriber implements PropertyDescriberInterface
{ {
public function describe(Type $type, Schema $property, array $groups = null) public function describe(Type $type, OA\Schema $property, array $groups = null)
{ {
$property->setType('integer'); $property->type = 'integer';
} }
public function supports(Type $type): bool public function supports(Type $type): bool

View File

@ -11,23 +11,21 @@
namespace Nelmio\ApiDocBundle\PropertyDescriber; namespace Nelmio\ApiDocBundle\PropertyDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\Model;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
class ObjectPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface class ObjectPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface
{ {
use ModelRegistryAwareTrait; use ModelRegistryAwareTrait;
public function describe(Type $type, Schema $property, array $groups = null) public function describe(Type $type, OA\Schema $property, array $groups = null)
{ {
$type = new Type($type->getBuiltinType(), false, $type->getClassName(), $type->isCollection(), $type->getCollectionKeyType(), $type->getCollectionValueType()); // ignore nullable field $type = new Type($type->getBuiltinType(), false, $type->getClassName(), $type->isCollection(), $type->getCollectionKeyType(), $type->getCollectionValueType()); // ignore nullable field
$property->setRef( $property->ref = $this->modelRegistry->register(new Model($type, $groups));
$this->modelRegistry->register(new Model($type, $groups))
);
} }
public function supports(Type $type): bool public function supports(Type $type): bool

View File

@ -11,7 +11,7 @@
namespace Nelmio\ApiDocBundle\PropertyDescriber; namespace Nelmio\ApiDocBundle\PropertyDescriber;
use EXSyst\Component\Swagger\Schema; use OpenApi\Annotations\Schema;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
interface PropertyDescriberInterface interface PropertyDescriberInterface

View File

@ -11,14 +11,14 @@
namespace Nelmio\ApiDocBundle\PropertyDescriber; namespace Nelmio\ApiDocBundle\PropertyDescriber;
use EXSyst\Component\Swagger\Schema; use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
class StringPropertyDescriber implements PropertyDescriberInterface class StringPropertyDescriber implements PropertyDescriberInterface
{ {
public function describe(Type $type, Schema $property, array $groups = null) public function describe(Type $type, OA\Schema $property, array $groups = null)
{ {
$property->setType('string'); $property->type = 'string';
} }
public function supports(Type $type): bool public function supports(Type $type): bool

View File

@ -10,6 +10,12 @@ Version](https://poser.pugx.org/nelmio/api-doc-bundle/v/stable)](https://packagi
The **NelmioApiDocBundle** bundle allows you to generate a decent documentation The **NelmioApiDocBundle** bundle allows you to generate a decent documentation
for your APIs. for your APIs.
## Migrate from 3.x to 4.0
[To migrate from 3.x to 4.0, follow our guide.](https://github.com/nelmio/NelmioApiDocBundle/blob/master/UPGRADE-4.0.md)
Version 4.0 brings OpenAPI 3.0 support. If you want to stick to Swagger 2.0, you should use the version 3 of this bundle.
## Migrate from 2.x to 3.0 ## Migrate from 2.x to 3.0
[To migrate from 2.x to 3.0, follow our guide.](https://github.com/nelmio/NelmioApiDocBundle/blob/master/UPGRADE-3.0.md) [To migrate from 2.x to 3.0, follow our guide.](https://github.com/nelmio/NelmioApiDocBundle/blob/master/UPGRADE-3.0.md)

View File

@ -6,6 +6,7 @@
<services> <services>
<service id="nelmio_api_doc.route_describers.fos_rest" class="Nelmio\ApiDocBundle\RouteDescriber\FosRestDescriber" public="false"> <service id="nelmio_api_doc.route_describers.fos_rest" class="Nelmio\ApiDocBundle\RouteDescriber\FosRestDescriber" public="false">
<argument type="service" id="annotation_reader" /> <argument type="service" id="annotation_reader" />
<argument />
<tag name="nelmio_api_doc.route_describer" priority="-250" /> <tag name="nelmio_api_doc.route_describer" priority="-250" />
</service> </service>

View File

@ -42,6 +42,7 @@
<argument type="service" id="property_info" /> <argument type="service" id="property_info" />
<argument type="service" id="annotation_reader" /> <argument type="service" id="annotation_reader" />
<argument type="tagged" tag="nelmio_api_doc.object_model.property_describer" /> <argument type="tagged" tag="nelmio_api_doc.object_model.property_describer" />
<argument />
<tag name="nelmio_api_doc.model_describer" /> <tag name="nelmio_api_doc.model_describer" />
</service> </service>

View File

@ -43,7 +43,7 @@ In this case the class ``App\Entity\User`` will be aliased into:
class HomeController class HomeController
{ {
/** /**
* @SWG\Response(response=200, @SWG\Schema(ref="#/definitions/MyModel")) * @OA\Response(response=200, @OA\JsonContent(@OA\Schema(ref="#/components/schemas/MyModel")))
*/ */
public function indexAction() public function indexAction()
{ {

View File

@ -6,39 +6,40 @@ Sharing parameter configuration
Q: I use the same value in multiple endpoints. How can I avoid duplicating the descriptions? Q: I use the same value in multiple endpoints. How can I avoid duplicating the descriptions?
A: You can configure ``definitions`` in the nelmio_api_doc configuration and then reference them: A: You can configure ``schemas`` in the nelmio_api_doc configuration and then reference them:
.. code-block:: yaml .. code-block:: yaml
# config/nelmio_api_doc.yml # config/nelmio_api_doc.yml
nelmio_api_doc: nelmio_api_doc:
documentation: documentation:
definitions: components:
NelmioImageList: schemas:
description: "Response for some queries" NelmioImageList:
type: object description: "Response for some queries"
properties: type: object
total: properties:
type: integer total:
example: 42 type: integer
items: example: 42
type: array
items: items:
$ref: "#/definitions/ImageMetadata" type: array
items:
$ref: "#/components/schemas/ImageMetadata"
.. code-block:: php .. code-block:: php
// src/App/Controller/NelmioController.php // src/App/Controller/NelmioController.php
/** /**
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="List of image definitions", * description="List of image definitions",
* @SWG\Schema( * @OA\JsonContent(@OA\Schema(
* type="object", * type="object",
* title="ListOperationsResponse", * title="ListOperationsResponse",
* additionalProperties={"$ref": "#/definitions/NelmioImageList"} * additionalProperties={"$ref": "#/components/schemas/NelmioImageList"}
* ) * ))
*/ */
Optional Path Parameters Optional Path Parameters
@ -55,7 +56,7 @@ optional? The controller might look like this::
* name="get_user_metadata" * name="get_user_metadata"
* ) * )
* *
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Json object with all user meta data or a json string with the value of the requested field" * description="Json object with all user meta data or a json string with the value of the requested field"
* ) * )
@ -76,14 +77,13 @@ separate actions in your controller. For example::
* name="get_user_metadata" * name="get_user_metadata"
* ) * )
* *
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Json hashmap with all user meta data", * description="Json hashmap with all user meta data",
* @SWG\Schema( * @OA\JsonContent(@OA\Schema(
* type="object", * type="object",
* example={"foo": "bar", "hello": "world"} * example={"foo": "bar", "hello": "world"}
* ) * ))
*
* ) * )
*/ */
public function cgetAction(string $user) public function cgetAction(string $user)
@ -99,12 +99,12 @@ separate actions in your controller. For example::
* name="get_user_metadata_single" * name="get_user_metadata_single"
* ) * )
* *
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="A json string with the value of the requested field", * description="A json string with the value of the requested field",
* @SWG\Schema( * @OA\JsonContent(@OA\Schema(
* type="string" * type="string"
* ) * ))
* ) * )
*/ */
public function getAction(string $user, string $metaName = null) public function getAction(string $user, string $metaName = null)
@ -184,14 +184,14 @@ Endpoints grouping
Q: Areas feature doesn't fit my needs. So how can I group similar endpoints of one or more controllers in a separate section in the documentation? Q: Areas feature doesn't fit my needs. So how can I group similar endpoints of one or more controllers in a separate section in the documentation?
A: Use ``@SWG\Tag`` annotation. A: Use ``@OA\Tag`` annotation.
.. code-block:: php .. code-block:: php
/** /**
* Class BookmarkController * Class BookmarkController
* *
* @SWG\Tag(name="Bookmarks") * @OA\Tag(name="Bookmarks")
*/ */
class BookmarkController extends AbstractFOSRestController implements ContextPresetInterface class BookmarkController extends AbstractFOSRestController implements ContextPresetInterface
{ {

View File

@ -99,28 +99,31 @@ Using the bundle
---------------- ----------------
You can configure global information in the bundle configuration ``documentation.info`` section (take a look at You can configure global information in the bundle configuration ``documentation.info`` section (take a look at
`the OpenAPI 2.0 specification (formerly Swagger)`_ to know the available fields): `the OpenAPI 3.0 specification (formerly Swagger)`_ to know the available fields):
.. code-block:: yaml .. code-block:: yaml
nelmio_api_doc: nelmio_api_doc:
documentation: documentation:
host: api.example.com servers:
schemes: [http, https] - url: http://api.example.com/unsafe
description: API over HTTP
- url: https://api.example.com/secured
description: API over HTTPS
info: info:
title: My App title: My App
description: This is an awesome app! description: This is an awesome app!
version: 1.0.0 version: 1.0.0
securityDefinitions: components:
Bearer: securitySchemes:
type: apiKey Bearer:
description: 'Value: Bearer {jwt}' type: http
name: Authorization scheme: bearer
in: header bearerFormat: JWT
security: security:
- Bearer: [] - Bearer: []
.. _`the OpenAPI 2.0 specification (formerly Swagger)`: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md .. _`the OpenAPI 3.0 specification (formerly Swagger)`: https://swagger.io/docs/specification
.. note:: .. note::
@ -135,7 +138,7 @@ To document your routes, you can use the SwaggerPHP annotations and the
use AppBundle\Entity\Reward; use AppBundle\Entity\Reward;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Security; use Nelmio\ApiDocBundle\Annotation\Security;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
class UserController class UserController
@ -146,21 +149,23 @@ To document your routes, you can use the SwaggerPHP annotations and the
* This call takes into account all confirmed awards, but not pending or refused awards. * This call takes into account all confirmed awards, but not pending or refused awards.
* *
* @Route("/api/{user}/rewards", methods={"GET"}) * @Route("/api/{user}/rewards", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Returns the rewards of an user", * description="Returns the rewards of an user",
* @SWG\Schema( * @OA\JsonContent(
* type="array", * @OA\Schema(
* @SWG\Items(ref=@Model(type=Reward::class, groups={"full"})) * type="array",
* @OA\Items(ref=@Model(type=Reward::class, groups={"full"}))
* )
* ) * )
* ) * )
* @SWG\Parameter( * @OA\Parameter(
* name="order", * name="order",
* in="query", * in="query",
* type="string", * type="string",
* description="The field used to order rewards" * description="The field used to order rewards"
* ) * )
* @SWG\Tag(name="rewards") * @OA\Tag(name="rewards")
* @Security(name="Bearer") * @Security(name="Bearer")
*/ */
public function fetchUserRewardsAction(User $user) public function fetchUserRewardsAction(User $user)
@ -186,7 +191,7 @@ This annotation has two options:
* ``type`` to specify your model's type:: * ``type`` to specify your model's type::
/** /**
    * @SWG\Response(     * @OA\Response(
    * response=200,     * response=200,
    *     @Model(type=User::class)     *     @Model(type=User::class)
    * )     * )
@ -195,7 +200,7 @@ This annotation has two options:
* ``groups`` to specify the serialization groups used to (de)serialize your model:: * ``groups`` to specify the serialization groups used to (de)serialize your model::
  /**   /**
    * @SWG\Response(     * @OA\Response(
    * response=200,     * response=200,
    *     @Model(type=User::class, groups={"non_sensitive_data"})     *     @Model(type=User::class, groups={"non_sensitive_data"})
    * )     * )
@ -203,23 +208,25 @@ This annotation has two options:
.. tip:: .. tip::
When used at the root of ``@SWG\Response`` and ``@SWG\Parameter``, ``@Model`` is automatically nested When used at the root of ``@OA\Response`` and ``@OA\Parameter``, ``@Model`` is automatically nested
in a ``@SWG\Schema``. in a ``@OA\Schema``.
To use ``@Model`` directly within a ``@SWG\Schema``, ``@SWG\Items`` or ``@SWG\Property``, you have to use the ``$ref`` field:: The media type defaults to ``application/json``.
To use ``@Model`` directly within a ``@OA\Schema``, ``@OA\Items`` or ``@OA\Property``, you have to use the ``$ref`` field::
/** /**
* @SWG\Response( * @OA\Response(
* @SWG\Schema(ref=@Model(type=User::class)) * @OA\JsonContent(ref=@Model(type=User::class))
* ) * )
* *
* or * or
* *
* @SWG\Response( * @OA\Response(@OA\XmlContent(
* @SWG\Schema(type="object", * @OA\Schema(type="object",
* @SWG\Property(property="foo", ref=@Model(type=FooClass::class)) * @OA\Property(property="foo", ref=@Model(type=FooClass::class))
* ) * )
* ) * ))
*/ */
Symfony Form types Symfony Form types
@ -234,9 +241,9 @@ You can customize the documentation of a form field using the ``documentation``
], ],
]); ]);
See the `OpenAPI 2.0 specification`__ to see all the available fields of the ``documentation`` option. See the `OpenAPI 3.0 specification`__ to see all the available fields of the ``documentation`` option.
__ https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject __ https://swagger.io/specification/
General PHP objects General PHP objects
@ -264,33 +271,33 @@ General PHP objects
When using the JMS serializer combined with `willdurand/Hateoas`_ (and the `BazingaHateoasBundle`_), When using the JMS serializer combined with `willdurand/Hateoas`_ (and the `BazingaHateoasBundle`_),
HATEOAS metadata are automatically extracted HATEOAS metadata are automatically extracted
If you want to customize the documentation of an object's property, you can use ``@SWG\Property``:: If you want to customize the documentation of an object's property, you can use ``@OA\Property``::
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
class User class User
{ {
/** /**
* @var int * @var int
* @SWG\Property(description="The unique identifier of the user.") * @OA\Property(description="The unique identifier of the user.")
*/ */
public $id; public $id;
/** /**
* @SWG\Property(type="string", maxLength=255) * @OA\Property(type="string", maxLength=255)
*/ */
public $username; public $username;
/** /**
* @SWG\Property(ref=@Model(type=User::class)) * @OA\Property(ref=@Model(type=User::class))
*/ */
public $friend; public $friend;
} }
See the `OpenAPI 2.0 specification`__ to see all the available fields of ``@SWG\Property``. See the `OpenAPI 3.0 specification`__ to see all the available fields of ``@OA\Property``.
__ https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject __ https://swagger.io/specification/
Learn more Learn more
---------- ----------

View File

@ -12,9 +12,10 @@
namespace Nelmio\ApiDocBundle\RouteDescriber; namespace Nelmio\ApiDocBundle\RouteDescriber;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Swagger;
use FOS\RestBundle\Controller\Annotations\QueryParam; use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\RequestParam; use FOS\RestBundle\Controller\Annotations\RequestParam;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Regex; use Symfony\Component\Validator\Constraints\Regex;
@ -23,17 +24,22 @@ final class FosRestDescriber implements RouteDescriberInterface
{ {
use RouteDescriberTrait; use RouteDescriberTrait;
/** @var Reader */
private $annotationReader; private $annotationReader;
public function __construct(Reader $annotationReader) /** @var string[] */
private $mediaTypes;
public function __construct(Reader $annotationReader, array $mediaTypes)
{ {
$this->annotationReader = $annotationReader; $this->annotationReader = $annotationReader;
$this->mediaTypes = $mediaTypes;
} }
public function describe(Swagger $api, Route $route, \ReflectionMethod $reflectionMethod) public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod)
{ {
$annotations = $this->annotationReader->getMethodAnnotations($reflectionMethod); $annotations = $this->annotationReader->getMethodAnnotations($reflectionMethod);
$annotations = array_filter($annotations, function ($value) { $annotations = array_filter($annotations, static function ($value) {
return $value instanceof RequestParam || $value instanceof QueryParam; return $value instanceof RequestParam || $value instanceof QueryParam;
}); });
@ -43,48 +49,32 @@ final class FosRestDescriber implements RouteDescriberInterface
if ($annotation instanceof QueryParam) { if ($annotation instanceof QueryParam) {
$name = $parameterName.($annotation->map ? '[]' : ''); $name = $parameterName.($annotation->map ? '[]' : '');
$parameter = $operation->getParameters()->get($name, 'query'); $parameter = Util::getOperationParameter($operation, $name, 'query');
$parameter->setAllowEmptyValue($annotation->nullable && $annotation->allowBlank); $parameter->allowEmptyValue = $annotation->nullable && $annotation->allowBlank;
$parameter->setRequired(!$annotation->nullable && $annotation->strict); $parameter->required = !$annotation->nullable && $annotation->strict;
} else {
$body = $operation->getParameters()->get('body', 'body')->getSchema();
$body->setType('object');
$parameter = $body->getProperties()->get($parameterName);
if (!$annotation->nullable && $annotation->strict) { if (OA\UNDEFINED === $parameter->description) {
$requiredParameters = $body->getRequired(); $parameter->description = $annotation->description;
$requiredParameters[] = $parameterName;
$body->setRequired(array_values(array_unique($requiredParameters)));
} }
}
$parameter->setDefault($annotation->getDefault()); $schema = Util::getChild($parameter, OA\Schema::class);
if (null !== $parameter->getType()) { $this->describeCommonSchemaFromAnnotation($schema, $annotation);
continue; } else {
} /** @var OA\RequestBody $requestBody */
$requestBody = Util::getChild($operation, OA\RequestBody::class);
foreach ($this->mediaTypes as $mediaType) {
$contentSchema = $this->getContentSchemaForType($requestBody, $mediaType);
$schema = Util::getProperty($contentSchema, $parameterName);
if (null === $parameter->getDescription()) { if (!$annotation->nullable && $annotation->strict) {
$parameter->setDescription($annotation->description); $requiredParameters = is_array($contentSchema->required) ? $contentSchema->required : [];
} $requiredParameters[] = $parameterName;
if ($annotation->map) { $contentSchema->required = array_values(array_unique($requiredParameters));
$parameter->setType('array'); }
$parameter->setCollectionFormat('multi'); $this->describeCommonSchemaFromAnnotation($schema, $annotation);
$parameter = $parameter->getItems(); }
}
$parameter->setType('string');
$pattern = $this->getPattern($annotation->requirements);
if (null !== $pattern) {
$parameter->setPattern($pattern);
}
$format = $this->getFormat($annotation->requirements);
if (null !== $format) {
$parameter->setFormat($format);
} }
} }
} }
@ -117,4 +107,64 @@ final class FosRestDescriber implements RouteDescriberInterface
return null; return null;
} }
private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema
{
$requestBody->content = OA\UNDEFINED !== $requestBody->content ? $requestBody->content : [];
switch ($type) {
case 'json':
$contentType = 'application\json';
break;
case 'xml':
$contentType = 'application\xml';
break;
default:
throw new \InvalidArgumentException('Unsupported media type');
}
if (!isset($requestBody->content[$contentType])) {
$requestBody->content[$contentType] = new OA\MediaType(
[
'mediaType' => $contentType,
]
);
/** @var OA\Schema $schema */
$schema = Util::getChild(
$requestBody->content[$contentType],
OA\Schema::class
);
$schema->type = 'object';
}
return Util::getChild(
$requestBody->content[$contentType],
OA\Schema::class
);
}
private function describeCommonSchemaFromAnnotation(OA\Schema $schema, $annotation)
{
$schema->default = $annotation->getDefault();
if (OA\UNDEFINED === $schema->type) {
$schema->type = $annotation->map ? 'array' : 'string';
}
if ($annotation->map) {
$schema->type = 'array';
$schema->collectionFormat = 'multi';
$schema->items = Util::getChild($schema, OA\Items::class);
}
$pattern = $this->getPattern($annotation->requirements);
if (null !== $pattern) {
$schema->pattern = $pattern;
}
$format = $this->getFormat($annotation->requirements);
if (null !== $format) {
$schema->format = $format;
}
}
} }

View File

@ -11,7 +11,7 @@
namespace Nelmio\ApiDocBundle\RouteDescriber; namespace Nelmio\ApiDocBundle\RouteDescriber;
use EXSyst\Component\Swagger\Swagger; use OpenApi\Annotations as OA;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\DocBlockFactoryInterface; use phpDocumentor\Reflection\DocBlockFactoryInterface;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
@ -30,7 +30,7 @@ final class PhpDocDescriber implements RouteDescriberInterface
$this->docBlockFactory = $docBlockFactory; $this->docBlockFactory = $docBlockFactory;
} }
public function describe(Swagger $api, Route $route, \ReflectionMethod $reflectionMethod) public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod)
{ {
$classDocBlock = null; $classDocBlock = null;
$docBlock = null; $docBlock = null;
@ -47,19 +47,19 @@ final class PhpDocDescriber implements RouteDescriberInterface
foreach ($this->getOperations($api, $route) as $operation) { foreach ($this->getOperations($api, $route) as $operation) {
if (null !== $docBlock) { if (null !== $docBlock) {
if (null === $operation->getSummary() && '' !== $docBlock->getSummary()) { if (OA\UNDEFINED === $operation->summary && '' !== $docBlock->getSummary()) {
$operation->setSummary($docBlock->getSummary()); $operation->summary = $docBlock->getSummary();
} }
if (null === $operation->getDescription() && '' !== (string) $docBlock->getDescription()) { if (OA\UNDEFINED === $operation->description && '' !== (string) $docBlock->getDescription()) {
$operation->setDescription((string) $docBlock->getDescription()); $operation->description = (string) $docBlock->getDescription();
} }
if ($docBlock->hasTag('deprecated')) { if ($docBlock->hasTag('deprecated')) {
$operation->setDeprecated(true); $operation->deprecated = true;
} }
} }
if (null !== $classDocBlock) { if (null !== $classDocBlock) {
if ($classDocBlock->hasTag('deprecated')) { if ($classDocBlock->hasTag('deprecated')) {
$operation->setDeprecated(true); $operation->deprecated = true;
} }
} }
} }

View File

@ -11,10 +11,10 @@
namespace Nelmio\ApiDocBundle\RouteDescriber; namespace Nelmio\ApiDocBundle\RouteDescriber;
use EXSyst\Component\Swagger\Swagger; use OpenApi\Annotations\OpenApi;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
interface RouteDescriberInterface interface RouteDescriberInterface
{ {
public function describe(Swagger $api, Route $route, \ReflectionMethod $reflectionMethod); public function describe(OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod);
} }

View File

@ -11,8 +11,9 @@
namespace Nelmio\ApiDocBundle\RouteDescriber; namespace Nelmio\ApiDocBundle\RouteDescriber;
use EXSyst\Component\Swagger\Operation; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use EXSyst\Component\Swagger\Swagger; use OpenApi\Annotations as OA;
use OpenApi\Annotations\OpenApi;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
/** /**
@ -23,20 +24,20 @@ trait RouteDescriberTrait
/** /**
* @internal * @internal
* *
* @return Operation[] * @return OA\Operation[]
*/ */
private function getOperations(Swagger $api, Route $route): array private function getOperations(OpenApi $api, Route $route): array
{ {
$operations = []; $operations = [];
$path = $api->getPaths()->get($this->normalizePath($route->getPath())); $path = Util::getPath($api, $this->normalizePath($route->getPath()));
$methods = $route->getMethods() ?: Swagger::$METHODS; $methods = $route->getMethods() ?: Util::OPERATIONS;
foreach ($methods as $method) { foreach ($methods as $method) {
$method = strtolower($method); $method = strtolower($method);
if (!in_array($method, Swagger::$METHODS)) { if (!in_array($method, Util::OPERATIONS)) {
continue; continue;
} }
$operations[] = $path->getOperation($method); $operations[] = Util::getOperation($path, $method);
} }
return $operations; return $operations;

View File

@ -11,10 +11,9 @@
namespace Nelmio\ApiDocBundle\RouteDescriber; namespace Nelmio\ApiDocBundle\RouteDescriber;
use EXSyst\Component\Swagger\Operation;
use EXSyst\Component\Swagger\Parameter;
use EXSyst\Component\Swagger\Swagger;
use LogicException; use LogicException;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
/** /**
@ -24,10 +23,10 @@ final class RouteMetadataDescriber implements RouteDescriberInterface
{ {
use RouteDescriberTrait; use RouteDescriberTrait;
public function describe(Swagger $api, Route $route, \ReflectionMethod $reflectionMethod) public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod)
{ {
foreach ($this->getOperations($api, $route) as $operation) { foreach ($this->getOperations($api, $route) as $operation) {
$operation->merge(['schemes' => $route->getSchemes()]); Util::merge($operation, ['security' => $route->getSchemes()]);
$requirements = $route->getRequirements(); $requirements = $route->getRequirements();
$compiledRoute = $route->compile(); $compiledRoute = $route->compile();
@ -40,24 +39,27 @@ final class RouteMetadataDescriber implements RouteDescriberInterface
} }
$paramId = $pathVariable.'/path'; $paramId = $pathVariable.'/path';
/** @var OA\Parameter $parameter */
$parameter = $existingParams[$paramId] ?? null; $parameter = $existingParams[$paramId] ?? null;
if (null !== $parameter) { if (null !== $parameter) {
if (!$parameter->getRequired()) { if (!$parameter->required || OA\UNDEFINED === $parameter->required) {
throw new LogicException(\sprintf('Global parameter "%s" is used as part of route "%s" and must be set as "required"', $pathVariable, $route->getPath())); throw new LogicException(\sprintf('Global parameter "%s" is used as part of route "%s" and must be set as "required"', $pathVariable, $route->getPath()));
} }
continue; continue;
} }
$parameter = $operation->getParameters()->get($pathVariable, 'path'); $parameter = Util::getOperationParameter($operation, $pathVariable, 'path');
$parameter->setRequired(true); $parameter->required = true;
if (null === $parameter->getType()) { $parameter->schema = Util::getChild($parameter, OA\Schema::class);
$parameter->setType('string');
if (OA\UNDEFINED === $parameter->schema->type) {
$parameter->schema->type = 'string';
} }
if (isset($requirements[$pathVariable]) && null === $parameter->getPattern()) { if (isset($requirements[$pathVariable]) && OA\UNDEFINED === $parameter->schema->pattern) {
$parameter->setPattern($requirements[$pathVariable]); $parameter->schema->pattern = $requirements[$pathVariable];
} }
} }
} }
@ -66,22 +68,24 @@ final class RouteMetadataDescriber implements RouteDescriberInterface
/** /**
* The '$ref' parameters need special handling, since their objects are missing 'name' and 'in'. * The '$ref' parameters need special handling, since their objects are missing 'name' and 'in'.
* *
* @return Parameter[] existing $ref parameters * @return OA\Parameter[] existing $ref parameters
*/ */
private function getRefParams(Swagger $api, Operation $operation): array private function getRefParams(OA\OpenApi $api, OA\Operation $operation): array
{ {
/** @var Parameter[] $globalParams */ /** @var OA\Parameter[] $globalParams */
$globalParams = $api->getParameters(); $globalParams = OA\UNDEFINED !== $api->components->parameters ? $api->components->parameters : [];
$existingParams = []; $existingParams = [];
foreach ($operation->getParameters() as $id => $parameter) { $operationParameters = OA\UNDEFINED !== $operation->parameters ? $operation->parameters : [];
$ref = $parameter->getRef(); /** @var OA\Parameter $parameter */
if (null === $ref) { foreach ($operationParameters as $id => $parameter) {
$ref = $parameter->ref;
if (OA\UNDEFINED === $ref) {
// we only concern ourselves with '$ref' parameters, so continue the loop // we only concern ourselves with '$ref' parameters, so continue the loop
continue; continue;
} }
$ref = \mb_substr($ref, 13); // trim the '#/parameters/' part of ref $ref = \mb_substr($ref, 24); // trim the '#/components/parameters/' part of ref
if (!isset($globalParams[$ref])) { if (!isset($globalParams[$ref])) {
// this shouldn't happen during proper configs, but in case of bad config, just ignore it here // this shouldn't happen during proper configs, but in case of bad config, just ignore it here
continue; continue;
@ -90,7 +94,7 @@ final class RouteMetadataDescriber implements RouteDescriberInterface
$refParameter = $globalParams[$ref]; $refParameter = $globalParams[$ref];
// param ids are in form {name}/{in} // param ids are in form {name}/{in}
$existingParams[\sprintf('%s/%s', $refParameter->getName(), $refParameter->getIn())] = $refParameter; $existingParams[\sprintf('%s/%s', $refParameter->name, $refParameter->in)] = $refParameter;
} }
return $existingParams; return $existingParams;

View File

@ -1,37 +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\SwaggerPhp;
use Swagger\Analysis;
use Swagger\Annotations\Info;
use Swagger\Annotations\Swagger;
use Swagger\Context;
/**
* Add defaults to fix default warnings.
*
* @internal
*/
final class AddDefaults
{
public function __invoke(Analysis $analysis)
{
if ($analysis->getAnnotationsOfType(Info::class)) {
return;
}
if (($annotations = $analysis->getAnnotationsOfType(Swagger::class)) && null !== $annotations[0]->info) {
return;
}
$analysis->addAnnotation(new Info(['title' => '', 'version' => '0.0.0', '_context' => new Context(['generated' => true])]), null);
}
}

View File

@ -1,130 +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\SwaggerPhp;
use Nelmio\ApiDocBundle\Annotation\Model as ModelAnnotation;
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Swagger\Analysis;
use Swagger\Annotations\AbstractAnnotation;
use Swagger\Annotations\Items;
use Swagger\Annotations\Parameter;
use Swagger\Annotations\Response;
use Swagger\Annotations\Schema;
use Symfony\Component\PropertyInfo\Type;
/**
* Resolves the path in SwaggerPhp annotation when needed.
*
* @internal
*/
final class ModelRegister
{
private $modelRegistry;
public function __construct(ModelRegistry $modelRegistry)
{
$this->modelRegistry = $modelRegistry;
}
public function __invoke(Analysis $analysis, array $parentGroups = null)
{
$modelsRegistered = [];
foreach ($analysis->annotations as $annotation) {
// @Model using the ref field
if ($annotation instanceof Schema && $annotation->ref instanceof ModelAnnotation) {
$model = $annotation->ref;
$annotation->ref = $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options));
// It is no longer an unmerged annotation
$this->detach($model, $annotation, $analysis);
continue;
}
// Implicit usages
if ($annotation instanceof Response) {
$annotationClass = Schema::class;
} elseif ($annotation instanceof Parameter) {
if ('array' === $annotation->type) {
$annotationClass = Items::class;
} else {
$annotationClass = Schema::class;
}
} elseif ($annotation instanceof Schema) {
$annotationClass = Items::class;
} else {
continue;
}
$model = null;
foreach ($annotation->_unmerged as $unmerged) {
if ($unmerged instanceof ModelAnnotation) {
$model = $unmerged;
break;
}
}
if (null === $model || !$model instanceof ModelAnnotation) {
continue;
}
if (!is_string($model->type)) {
// Ignore invalid annotations, they are validated later
continue;
}
if ($annotation instanceof Schema) {
@trigger_error(sprintf('Using `@Model` implicitly in a `@SWG\Schema`, `@SWG\Items` or `@SWG\Property` annotation in %s is deprecated since version 3.2 and won\'t be supported in 4.0. Use `ref=@Model()` instead.', $annotation->_context->getDebugLocation()), E_USER_DEPRECATED);
}
$annotation->merge([new $annotationClass([
'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options)),
])]);
// It is no longer an unmerged annotation
$this->detach($model, $annotation, $analysis);
}
}
private function getGroups(ModelAnnotation $model, array $parentGroups = null)
{
if (null === $model->groups) {
return $parentGroups;
}
return array_merge($parentGroups ?? [], $model->groups);
}
private function detach(ModelAnnotation $model, AbstractAnnotation $annotation, Analysis $analysis)
{
foreach ($annotation->_unmerged as $key => $unmerged) {
if ($unmerged === $model) {
unset($annotation->_unmerged[$key]);
break;
}
}
$analysis->annotations->detach($model);
}
private function createType(string $type): Type
{
if ('[]' === substr($type, -2)) {
return new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, $this->createType(substr($type, 0, -2)));
}
return new Type(Type::BUILTIN_TYPE_OBJECT, false, $type);
}
}

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('swagger_doc')->get()); $this->assertEquals($generator->generate(), $adapter->getItem('openapi_doc')->get());
} }
public function testCacheWithCustomId() public function testCacheWithCustomId()

View File

@ -11,8 +11,8 @@
namespace Nelmio\ApiDocBundle\Tests\Describer; namespace Nelmio\ApiDocBundle\Tests\Describer;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Describer\DescriberInterface; use Nelmio\ApiDocBundle\Describer\DescriberInterface;
use OpenApi\Annotations\OpenApi;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
abstract class AbstractDescriberTest extends TestCase abstract class AbstractDescriberTest extends TestCase
@ -20,9 +20,9 @@ abstract class AbstractDescriberTest extends TestCase
/** @var DescriberInterface */ /** @var DescriberInterface */
protected $describer; protected $describer;
protected function getSwaggerDoc(): Swagger protected function getOpenApiDoc(): OpenApi
{ {
$api = new Swagger(); $api = new OpenApi([]);
$this->describer->describe($api); $this->describer->describe($api);
return $api; return $api;

View File

@ -13,8 +13,8 @@ namespace Nelmio\ApiDocBundle\Tests\Describer;
use ApiPlatform\Core\Documentation\Documentation; use ApiPlatform\Core\Documentation\Documentation;
use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection; use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Describer\ApiPlatformDescriber; use Nelmio\ApiDocBundle\Describer\ApiPlatformDescriber;
use OpenApi\Annotations\OpenApi;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class ApiPlatformDescriberTest extends AbstractDescriberTest class ApiPlatformDescriberTest extends AbstractDescriberTest
@ -30,8 +30,8 @@ class ApiPlatformDescriberTest extends AbstractDescriberTest
->with($this->documentation) ->with($this->documentation)
->willReturn(['info' => ['title' => 'My Test App']]); ->willReturn(['info' => ['title' => 'My Test App']]);
$expectedApi = new Swagger(['info' => ['title' => 'My Test App']]); $expectedApi = new OpenApi(['info' => ['title' => 'My Test App']]);
$this->assertEquals($expectedApi->toArray(), $this->getSwaggerDoc()->toArray()); $this->assertEquals($expectedApi->toJson(), $this->getOpenApiDoc()->toJson());
} }
public function testDescribeRemovesBasePathAfterNormalization() public function testDescribeRemovesBasePathAfterNormalization()
@ -41,8 +41,8 @@ class ApiPlatformDescriberTest extends AbstractDescriberTest
->with($this->documentation) ->with($this->documentation)
->willReturn(['info' => ['title' => 'My Test App'], 'basePath' => '/foo']); ->willReturn(['info' => ['title' => 'My Test App'], 'basePath' => '/foo']);
$expectedApi = new Swagger(['info' => ['title' => 'My Test App']]); $expectedApi = new OpenApi(['info' => ['title' => 'My Test App']]);
$this->assertEquals($expectedApi->toArray(), $this->getSwaggerDoc()->toArray()); $this->assertEquals($expectedApi->toJson(), $this->getOpenApiDoc()->toJson());
} }
protected function setUp() protected function setUp()

View File

@ -11,10 +11,10 @@
namespace Nelmio\ApiDocBundle\Tests\Describer; namespace Nelmio\ApiDocBundle\Tests\Describer;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Describer\RouteDescriber; use Nelmio\ApiDocBundle\Describer\RouteDescriber;
use Nelmio\ApiDocBundle\RouteDescriber\RouteDescriberInterface; use Nelmio\ApiDocBundle\RouteDescriber\RouteDescriberInterface;
use Nelmio\ApiDocBundle\Util\ControllerReflector; use Nelmio\ApiDocBundle\Util\ControllerReflector;
use OpenApi\Annotations\OpenApi;
use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser;
use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
@ -32,7 +32,7 @@ class RouteDescriberTest extends AbstractDescriberTest
$this->routeDescriber->expects($this->never()) $this->routeDescriber->expects($this->never())
->method('describe'); ->method('describe');
$this->assertEquals((new Swagger())->toArray(), $this->getSwaggerDoc()->toArray()); $this->assertEquals((new OpenApi([]))->toJson(), $this->getOpenApiDoc()->toJson());
} }
protected function setUp() protected function setUp()

View File

@ -72,7 +72,8 @@ class BazingaFunctionalTest extends WebTestCase
], ],
], ],
], ],
], $this->getModel('BazingaUser')->toArray()); 'schema' => 'BazingaUser',
], json_decode($this->getModel('BazingaUser')->toJson(), true));
} }
public function testWithGroup() public function testWithGroup()
@ -89,7 +90,8 @@ class BazingaFunctionalTest extends WebTestCase
], ],
], ],
], ],
], $this->getModel('BazingaUser_grouped')->toArray()); 'schema' => 'BazingaUser_grouped',
], json_decode($this->getModel('BazingaUser_grouped')->toJson(), true));
} }
public function testWithType() public function testWithType()
@ -107,7 +109,7 @@ class BazingaFunctionalTest extends WebTestCase
'properties' => [ 'properties' => [
'typed_bazinga_users' => [ 'typed_bazinga_users' => [
'items' => [ 'items' => [
'$ref' => '#/definitions/BazingaUser', '$ref' => '#/components/schemas/BazingaUser',
], ],
'type' => 'array', 'type' => 'array',
], ],
@ -117,7 +119,8 @@ class BazingaFunctionalTest extends WebTestCase
], ],
], ],
], ],
], $this->getModel('BazingaUserTyped')->toArray()); 'schema' => 'BazingaUserTyped',
], json_decode($this->getModel('BazingaUserTyped')->toJson(), true));
} }
protected static function createKernel(array $options = []) protected static function createKernel(array $options = [])

View File

@ -20,7 +20,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\User; use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType; use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType;
use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType; use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
/** /**
@ -29,12 +29,12 @@ use Symfony\Component\Routing\Annotation\Route;
class ApiController class ApiController
{ {
/** /**
* @SWG\Response( * @OA\Response(
* response="200", * response="200",
* description="Success", * description="Success",
* @SWG\Schema(ref=@Model(type=Article::class, groups={"light"})) * @Model(type=Article::class, groups={"light"}))
* ) * )
* @SWG\Parameter(ref="#/parameters/test") * @OA\Parameter(ref="#/components/parameters/test")
* @Route("/article/{id}", methods={"GET"}) * @Route("/article/{id}", methods={"GET"})
*/ */
public function fetchArticleAction() public function fetchArticleAction()
@ -47,7 +47,7 @@ class ApiController
* @Route("/swagger", methods={"GET", "LINK"}) * @Route("/swagger", methods={"GET", "LINK"})
* @Route("/swagger2", methods={"GET"}) * @Route("/swagger2", methods={"GET"})
* @Operation( * @Operation(
* @SWG\Response(response="201", description="An example resource") * @OA\Response(response="201", description="An example resource")
* ) * )
*/ */
public function swaggerAction() public function swaggerAction()
@ -56,21 +56,19 @@ class ApiController
/** /**
* @Route("/swagger/implicit", methods={"GET", "POST"}) * @Route("/swagger/implicit", methods={"GET", "POST"})
* @SWG\Response( * @OA\Response(
* response="201", * response="201",
* description="Operation automatically detected", * description="Operation automatically detected",
* @Model(type=User::class) * @Model(type=User::class)
* ),
* @OA\RequestBody(
* description="This is a request body",
* @OA\JsonContent(
* type="array",
* @OA\Items(ref=@Model(type=User::class))
* )
* ) * )
* @SWG\Parameter( * @OA\Tag(name="implicit")
* name="foo",
* in="body",
* description="This is a parameter",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref=@Model(type=User::class))
* )
* )
* @SWG\Tag(name="implicit")
*/ */
public function implicitSwaggerAction() public function implicitSwaggerAction()
{ {
@ -78,16 +76,14 @@ class ApiController
/** /**
* @Route("/test/users/{user}", methods={"POST"}, schemes={"https"}, requirements={"user"="/foo/"}) * @Route("/test/users/{user}", methods={"POST"}, schemes={"https"}, requirements={"user"="/foo/"})
* @SWG\Response( * @OA\Response(
* response="201", * response="201",
* description="Operation automatically detected", * description="Operation automatically detected",
* @Model(type=User::class) * @Model(type=User::class)
* ) * ),
* @SWG\Parameter( * @OA\RequestBody(
* name="foo", * description="This is a request body",
* in="body", * @Model(type=UserType::class, options={"bar": "baz"}))
* description="This is a parameter",
* @SWG\Schema(ref=@Model(type=UserType::class, options={"bar": "baz"}))
* ) * )
*/ */
public function submitUserTypeAction() public function submitUserTypeAction()
@ -96,9 +92,7 @@ class ApiController
/** /**
* @Route("/test/{user}", methods={"GET"}, schemes={"https"}, requirements={"user"="/foo/"}) * @Route("/test/{user}", methods={"GET"}, schemes={"https"}, requirements={"user"="/foo/"})
* @Operation( * @OA\Response(response=200, description="sucessful")
* @SWG\Response(response=200, description="sucessful")
* )
*/ */
public function userAction() public function userAction()
{ {
@ -127,9 +121,9 @@ class ApiController
} }
/** /**
* @SWG\Get( * @OA\Get(
* path="/filtered", * path="/filtered",
* @SWG\Response(response="201", description="") * @OA\Response(response="201", description="")
* ) * )
*/ */
public function filteredAction() public function filteredAction()
@ -138,13 +132,11 @@ class ApiController
/** /**
* @Route("/form", methods={"POST"}) * @Route("/form", methods={"POST"})
* @SWG\Parameter( * @OA\RequestBody(
* name="form", * description="Request content",
* in="body", * @Model(type=DummyType::class))
* description="Request content",
* @SWG\Schema(ref=@Model(type=DummyType::class))
* ) * )
* @SWG\Response(response="201", description="") * @OA\Response(response="201", description="")
*/ */
public function formAction() public function formAction()
{ {
@ -152,7 +144,7 @@ class ApiController
/** /**
* @Route("/security") * @Route("/security")
* @SWG\Response(response="201", description="") * @OA\Response(response="201", description="")
* @Security(name="api_key") * @Security(name="api_key")
* @Security(name="basic") * @Security(name="basic")
*/ */
@ -162,10 +154,10 @@ class ApiController
/** /**
* @Route("/swagger/symfonyConstraints", methods={"GET"}) * @Route("/swagger/symfonyConstraints", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response="201", * response="201",
* description="Used for symfony constraints test", * description="Used for symfony constraints test",
* @SWG\Schema(ref=@Model(type=SymfonyConstraints::class)) * @Model(type=SymfonyConstraints::class)
* ) * )
*/ */
public function symfonyConstraintsAction() public function symfonyConstraintsAction()
@ -173,15 +165,15 @@ class ApiController
} }
/** /**
* @SWG\Response( * @OA\Response(
* response="200", * response="200",
* description="Success", * description="Success",
* @SWG\Schema(ref="#/definitions/Test") * ref="#/components/schemas/Test"
* ) * ),
* @SWG\Response( * @OA\Response(
* response="201", * response="201",
* ref="#/responses/201" * ref="#/components/responses/201"
* ) * )
* @Route("/configReference", methods={"GET"}) * @Route("/configReference", methods={"GET"})
*/ */
public function configReferenceAction() public function configReferenceAction()
@ -190,10 +182,10 @@ class ApiController
/** /**
* @Route("/multi-annotations", methods={"GET", "POST"}) * @Route("/multi-annotations", methods={"GET", "POST"})
* @SWG\Get(description="This is the get operation") * @OA\Get(description="This is the get operation")
* @SWG\Post(description="This is post") * @OA\Post(description="This is post")
* *
* @SWG\Response(response=200, description="Worked well!", @Model(type=DummyType::class)) * @OA\Response(response=200, description="Worked well!", @Model(type=DummyType::class))
*/ */
public function operationsWithOtherAnnotations() public function operationsWithOtherAnnotations()
{ {

View File

@ -13,7 +13,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\BazingaUser; use Nelmio\ApiDocBundle\Tests\Functional\Entity\BazingaUser;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
/** /**
@ -23,7 +23,7 @@ class BazingaController
{ {
/** /**
* @Route("/api/bazinga", methods={"GET"}) * @Route("/api/bazinga", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=BazingaUser::class) * @Model(type=BazingaUser::class)
@ -35,7 +35,7 @@ class BazingaController
/** /**
* @Route("/api/bazinga_foo", methods={"GET"}) * @Route("/api/bazinga_foo", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=BazingaUser::class, groups={"foo"}) * @Model(type=BazingaUser::class, groups={"foo"})

View File

@ -13,7 +13,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Tests\Functional\EntityExcluded\BazingaUserTyped; use Nelmio\ApiDocBundle\Tests\Functional\EntityExcluded\BazingaUserTyped;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
/** /**
@ -23,7 +23,7 @@ class BazingaTypedController
{ {
/** /**
* @Route("/api/bazinga_typed", methods={"GET"}) * @Route("/api/bazinga_typed", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=BazingaUserTyped::class) * @Model(type=BazingaUserTyped::class)

View File

@ -12,7 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller; namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use Nelmio\ApiDocBundle\Annotation\Security; use Nelmio\ApiDocBundle\Annotation\Security;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
/** /**
@ -23,7 +23,7 @@ class ClassApiController
{ {
/** /**
* @Route("/security/class") * @Route("/security/class")
* @SWG\Response(response="201", description="") * @OA\Response(response="201", description="")
*/ */
public function securityAction() public function securityAction()
{ {

View File

@ -21,7 +21,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatRoomUser;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatUser; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatUser;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\VirtualProperty; use Nelmio\ApiDocBundle\Tests\Functional\Entity\VirtualProperty;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
/** /**
@ -31,7 +31,7 @@ class JMSController
{ {
/** /**
* @Route("/api/jms", methods={"GET"}) * @Route("/api/jms", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=JMSUser::class) * @Model(type=JMSUser::class)
@ -43,7 +43,7 @@ class JMSController
/** /**
* @Route("/api/yaml", methods={"GET"}) * @Route("/api/yaml", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=VirtualProperty::class) * @Model(type=VirtualProperty::class)
@ -55,7 +55,7 @@ class JMSController
/** /**
* @Route("/api/jms_complex", methods={"GET"}) * @Route("/api/jms_complex", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=JMSComplex::class, groups={"list", "details", "User" : {"list"}}) * @Model(type=JMSComplex::class, groups={"list", "details", "User" : {"list"}})
@ -67,7 +67,7 @@ class JMSController
/** /**
* @Route("/api/jms_complex_dual", methods={"GET"}) * @Route("/api/jms_complex_dual", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=JMSDualComplex::class, groups={"Default", "complex" : {"User" : {"details"}}}) * @Model(type=JMSDualComplex::class, groups={"Default", "complex" : {"User" : {"details"}}})
@ -79,7 +79,7 @@ class JMSController
/** /**
* @Route("/api/jms_naming_strategy", methods={"GET"}) * @Route("/api/jms_naming_strategy", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=JMSNamingStrategyConstraints::class, groups={"Default"}) * @Model(type=JMSNamingStrategyConstraints::class, groups={"Default"})
@ -91,7 +91,7 @@ class JMSController
/** /**
* @Route("/api/jms_chat", methods={"GET"}) * @Route("/api/jms_chat", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=JMSChat::class, groups={"Default", "members" : {"mini"}}) * @Model(type=JMSChat::class, groups={"Default", "members" : {"mini"}})
@ -103,7 +103,7 @@ class JMSController
/** /**
* @Route("/api/jms_picture", methods={"GET"}) * @Route("/api/jms_picture", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=JMSPicture::class, groups={"mini"}) * @Model(type=JMSPicture::class, groups={"mini"})
@ -115,7 +115,7 @@ class JMSController
/** /**
* @Route("/api/jms_mini_user", methods={"GET"}) * @Route("/api/jms_mini_user", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=JMSChatUser::class, groups={"mini"}) * @Model(type=JMSChatUser::class, groups={"mini"})
@ -127,7 +127,7 @@ class JMSController
/** /**
* @Route("/api/jms_mini_user_nested", methods={"GET"}) * @Route("/api/jms_mini_user_nested", methods={"GET"})
* @SWG\Response( * @OA\Response(
* response=200, * response=200,
* description="Success", * description="Success",
* @Model(type=JMSChatRoomUser::class, groups={"mini", "friend": {"living":{"Default"}}}) * @Model(type=JMSChatRoomUser::class, groups={"mini", "friend": {"living":{"Default"}}})

View File

@ -11,7 +11,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller; namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
/** /**
@ -20,7 +20,7 @@ use Symfony\Component\Routing\Annotation\Route;
class TestController class TestController
{ {
/** /**
* @SWG\Response( * @OA\Response(
* response="200", * response="200",
* description="Test" * description="Test"
* ) * )
@ -31,8 +31,8 @@ class TestController
} }
/** /**
* @SWG\Parameter(ref="#/parameters/test"), * @OA\Parameter(ref="#/components/parameters/test"),
* @SWG\Response( * @OA\Response(
* response="200", * response="200",
* description="Test Ref" * description="Test Ref"
* ) * )

View File

@ -13,13 +13,13 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
/** /**
* @Serializer\ExclusionPolicy("all") * @Serializer\ExclusionPolicy("all")
* @SWG\Definition( * @OA\Schema(
* required={"id", "user"}, * required={"id", "user"},
* @SWG\Property(property="virtual", ref=@Model(type=JMSUser::class)) * @OA\Property(property="virtual", ref=@Model(type=JMSUser::class))
* ) * )
*/ */
class JMSComplex class JMSComplex
@ -32,7 +32,7 @@ class JMSComplex
private $id; private $id;
/** /**
* @SWG\Property(ref=@Model(type=JMSUser::class)) * @OA\Property(ref=@Model(type=JMSUser::class))
* @Serializer\Expose * @Serializer\Expose
* @Serializer\Groups({"details"}) * @Serializer\Groups({"details"})
* @Serializer\SerializedName("user") * @Serializer\SerializedName("user")

View File

@ -13,7 +13,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
class JMSDualComplex class JMSDualComplex
{ {
@ -23,12 +23,12 @@ class JMSDualComplex
private $id; private $id;
/** /**
* @SWG\Property(ref=@Model(type=JMSComplex::class)) * @OA\Property(ref=@Model(type=JMSComplex::class))
*/ */
private $complex; private $complex;
/** /**
* @SWG\Property(ref=@Model(type=JMSUser::class)) * @OA\Property(ref=@Model(type=JMSUser::class))
*/ */
private $user; private $user;
} }

View File

@ -12,7 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
/** /**
* User. * User.
@ -26,7 +26,7 @@ class JMSUser
* @Serializer\Expose * @Serializer\Expose
* @Serializer\Groups({"list"}) * @Serializer\Groups({"list"})
* *
* @SWG\Property(description = "User id", readOnly = true, title = "userid", example=1, default = null) * @OA\Property(description = "User id", readOnly = true, title = "userid", example=1, default = null)
*/ */
private $id; private $id;
@ -35,14 +35,14 @@ class JMSUser
* @Serializer\Expose * @Serializer\Expose
* @Serializer\SerializedName("daysOnline") * @Serializer\SerializedName("daysOnline")
* *
* @SWG\Property(default = 0, minimum = 1, maximum = 300) * @OA\Property(default = 0, minimum = 1, maximum = 300)
*/ */
private $daysOnline; private $daysOnline;
/** /**
* @Serializer\Type("string") * @Serializer\Type("string")
* @Serializer\Expose * @Serializer\Expose
* @SWG\Property(readOnly = false) * @OA\Property(readOnly = false)
* @Serializer\Groups({"details"}) * @Serializer\Groups({"details"})
*/ */
private $email; private $email;
@ -52,7 +52,7 @@ class JMSUser
* @Serializer\Accessor(getter="getRoles", setter="setRoles") * @Serializer\Accessor(getter="getRoles", setter="setRoles")
* @Serializer\Expose * @Serializer\Expose
* *
* @SWG\Property(default = {"user"}, description = "Roles list", example="[""ADMIN"",""SUPERUSER""]", title="roles") * @OA\Property(default = {"user"}, description = "Roles list", example="[""ADMIN"",""SUPERUSER""]", title="roles")
*/ */
private $roles; private $roles;
@ -62,7 +62,7 @@ class JMSUser
private $password; private $password;
/** /**
* @SWG\Property(property="last_update", type="date") * @OA\Property(property="last_update", type="date")
* @Serializer\Expose * @Serializer\Expose
*/ */
private $updatedAt; private $updatedAt;
@ -103,7 +103,7 @@ class JMSUser
* @Serializer\Expose * @Serializer\Expose
* @Serializer\SerializedName("friendsNumber") * @Serializer\SerializedName("friendsNumber")
* *
* @SWG\Property(type = "string", minLength = 1, maxLength = 100) * @OA\Property(type = "string", minLength = 1, maxLength = 100)
*/ */
private $friendsNumber; private $friendsNumber;
@ -122,7 +122,7 @@ class JMSUser
* @Serializer\Type("string") * @Serializer\Type("string")
* @Serializer\Expose * @Serializer\Expose
* *
* @SWG\Property(enum = {"disabled", "enabled"}) * @OA\Property(enum = {"disabled", "enabled"})
*/ */
private $status; private $status;

View File

@ -11,7 +11,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use Swagger\Annotations as SWG; use OpenApi\Annotations as OA;
/** /**
* @author Guilhem N. <egetick@gmail.com> * @author Guilhem N. <egetick@gmail.com>
@ -21,19 +21,19 @@ class User
/** /**
* @var int * @var int
* *
* @SWG\Property(description = "User id", readOnly = true, title = "userid", example=1, default = null) * @OA\Property(description = "User id", readOnly = true, title = "userid", example=1, default = null)
*/ */
private $id; private $id;
/** /**
* @SWG\Property(type="string", readOnly = false) * @OA\Property(type="string", readOnly = false)
*/ */
private $email; private $email;
/** /**
* @var string[] * @var string[]
* *
* @SWG\Property( * @OA\Property(
* description = "User roles", * description = "User roles",
* title = "roles", * title = "roles",
* example="[""ADMIN"",""SUPERUSER""]", * example="[""ADMIN"",""SUPERUSER""]",
@ -45,19 +45,19 @@ class User
/** /**
* @var int * @var int
* *
* @SWG\Property(type = "string") * @OA\Property(type = "string")
*/ */
private $friendsNumber; private $friendsNumber;
/** /**
* @var float * @var float
* @SWG\Property(default = 0.0) * @OA\Property(default = 0.0)
*/ */
private $money; private $money;
/** /**
* @var \DateTime * @var \DateTime
* @SWG\Property(property="creationDate") * @OA\Property(property="creationDate")
*/ */
private $createdAt; private $createdAt;
@ -74,7 +74,7 @@ class User
/** /**
* @var string * @var string
* *
* @SWG\Property(enum = {"disabled", "enabled"}) * @OA\Property(enum = {"disabled", "enabled"})
*/ */
private $status; private $status;

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
use FOS\RestBundle\FOSRestBundle; use FOS\RestBundle\FOSRestBundle;
use OpenApi\Annotations as OA;
class FOSRestTest extends WebTestCase class FOSRestTest extends WebTestCase
{ {
@ -30,30 +31,28 @@ class FOSRestTest extends WebTestCase
$operation = $this->getOperation('/api/fosrest', 'post'); $operation = $this->getOperation('/api/fosrest', 'post');
$parameters = $operation->getParameters(); $this->assertHasParameter('foo', 'query', $operation);
$this->assertTrue($parameters->has('foo', 'query')); $this->assertInstanceOf(OA\RequestBody::class, $operation->requestBody);
$this->assertTrue($parameters->has('body', 'body'));
$body = $parameters->get('body', 'body')->getSchema()->getProperties();
$this->assertTrue($body->has('bar')); $bodySchema = $operation->requestBody->content['application\json']->schema;
$this->assertTrue($body->has('baz'));
$fooParameter = $parameters->get('foo', 'query'); $this->assertHasProperty('bar', $bodySchema);
$this->assertNotNull($fooParameter->getPattern()); $this->assertHasProperty('baz', $bodySchema);
$this->assertEquals('\d+', $fooParameter->getPattern());
$this->assertNull($fooParameter->getFormat());
$barParameter = $body->get('bar'); $fooParameter = $this->getParameter($operation, 'foo', 'query');
$this->assertNotNull($barParameter->getPattern()); $this->assertInstanceOf(OA\Schema::class, $fooParameter->schema);
$this->assertEquals('\d+', $barParameter->getPattern()); $this->assertEquals('\d+', $fooParameter->schema->pattern);
$this->assertNull($barParameter->getFormat()); $this->assertEquals(OA\UNDEFINED, $fooParameter->schema->format);
$bazParameter = $body->get('baz'); $barProperty = $this->getProperty($bodySchema, 'bar');
$this->assertNotNull($bazParameter->getFormat()); $this->assertEquals('\d+', $barProperty->pattern);
$this->assertEquals('IsTrue', $bazParameter->getFormat()); $this->assertEquals(OA\UNDEFINED, $barProperty->format);
$this->assertNull($bazParameter->getPattern());
$bazProperty = $this->getProperty($bodySchema, 'baz');
$this->assertEquals(OA\UNDEFINED, $bazProperty->pattern);
$this->assertEquals('IsTrue', $bazProperty->format);
// The _format path attribute should be removed // The _format path attribute should be removed
$this->assertFalse($parameters->has('_format', 'path')); $this->assertNotHasParameter('_format', 'path', $operation);
} }
} }

View File

@ -11,7 +11,8 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
use EXSyst\Component\Swagger\Tag; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
class FunctionalTest extends WebTestCase class FunctionalTest extends WebTestCase
{ {
@ -24,39 +25,39 @@ class FunctionalTest extends WebTestCase
public function testConfiguredDocumentation() public function testConfiguredDocumentation()
{ {
$this->assertEquals('My Default App', $this->getSwaggerDefinition()->getInfo()->getTitle()); $this->assertEquals('My Default App', $this->getOpenApiDefinition()->info->title);
$this->assertEquals('My Test App', $this->getSwaggerDefinition('test')->getInfo()->getTitle()); $this->assertEquals('My Test App', $this->getOpenApiDefinition('test')->info->title);
} }
public function testUndocumentedAction() public function testUndocumentedAction()
{ {
$paths = $this->getSwaggerDefinition()->getPaths(); $api = $this->getOpenApiDefinition();
$this->assertFalse($paths->has('/undocumented'));
$this->assertFalse($paths->has('/api/admin')); $this->assertNotHasPath('/undocumented', $api);
$this->assertNotHasPath('/api/admin', $api);
} }
public function testFetchArticleAction() public function testFetchArticleAction()
{ {
$operation = $this->getOperation('/api/article/{id}', 'get'); $operation = $this->getOperation('/api/article/{id}', 'get');
$responses = $operation->getResponses(); $this->assertHasResponse('200', $operation);
$this->assertTrue($responses->has('200')); $response = $this->getOperationResponse($operation, '200');
$this->assertEquals('#/definitions/Article', $responses->get('200')->getSchema()->getRef()); $this->assertEquals('#/components/schemas/Article', $response->content['application/json']->schema->ref);
// Ensure that groups are supported // Ensure that groups are supported
$modelProperties = $this->getModel('Article')->getProperties(); $articleModel = $this->getModel('Article');
$this->assertCount(1, $modelProperties); $this->assertCount(1, $articleModel->properties);
$this->assertTrue($modelProperties->has('author')); $this->assertHasProperty('author', $articleModel);
$this->assertSame('#/definitions/User2', $modelProperties->get('author')->getRef()); $this->assertSame('#/components/schemas/User2', Util::getProperty($articleModel, 'author')->ref);
$this->assertNotHasProperty('author', Util::getProperty($articleModel, 'author'));
$this->assertFalse($modelProperties->has('content'));
} }
public function testFilteredAction() public function testFilteredAction()
{ {
$paths = $this->getSwaggerDefinition()->getPaths(); $openApi = $this->getOpenApiDefinition();
$this->assertFalse($paths->has('/filtered')); $this->assertNotHasPath('/filtered', $openApi);
} }
/** /**
@ -64,13 +65,13 @@ class FunctionalTest extends WebTestCase
* *
* @dataProvider swaggerActionPathsProvider * @dataProvider swaggerActionPathsProvider
*/ */
public function testSwaggerAction($path) public function testSwaggerAction(string $path)
{ {
$operation = $this->getOperation($path, 'get'); $operation = $this->getOperation($path, 'get');
$responses = $operation->getResponses(); $this->assertHasResponse('201', $operation);
$this->assertTrue($responses->has('201')); $response = $this->getOperationResponse($operation, '201');
$this->assertEquals('An example resource', $responses->get('201')->getDescription()); $this->assertEquals('An example resource', $response->description);
} }
public function swaggerActionPathsProvider() public function swaggerActionPathsProvider()
@ -81,24 +82,22 @@ class FunctionalTest extends WebTestCase
/** /**
* @dataProvider implicitSwaggerActionMethodsProvider * @dataProvider implicitSwaggerActionMethodsProvider
*/ */
public function testImplicitSwaggerAction($method) public function testImplicitSwaggerAction(string $method)
{ {
$operation = $this->getOperation('/api/swagger/implicit', $method); $operation = $this->getOperation('/api/swagger/implicit', $method);
$this->assertEquals([new Tag('implicit')], $operation->getTags()); $this->assertEquals(['implicit'], $operation->tags);
$responses = $operation->getResponses(); $this->assertHasResponse('201', $operation);
$this->assertTrue($responses->has('201')); $response = $this->getOperationResponse($operation, '201');
$response = $responses->get('201'); $this->assertEquals('Operation automatically detected', $response->description);
$this->assertEquals('Operation automatically detected', $response->getDescription()); $this->assertEquals('#/components/schemas/User', $response->content['application/json']->schema->ref);
$this->assertEquals('#/definitions/User', $response->getSchema()->getRef());
$parameters = $operation->getParameters(); $this->assertInstanceOf(OA\RequestBody::class, $operation->requestBody);
$this->assertTrue($parameters->has('foo', 'body')); $requestBody = $operation->requestBody;
$parameter = $parameters->get('foo', 'body'); $this->assertEquals('This is a request body', $requestBody->description);
$this->assertEquals('array', $requestBody->content['application/json']->schema->type);
$this->assertEquals('This is a parameter', $parameter->getDescription()); $this->assertEquals('#/components/schemas/User', $requestBody->content['application/json']->schema->items->ref);
$this->assertEquals('#/definitions/User', $parameter->getSchema()->getItems()->getRef());
} }
public function implicitSwaggerActionMethodsProvider() public function implicitSwaggerActionMethodsProvider()
@ -110,29 +109,27 @@ class FunctionalTest extends WebTestCase
{ {
$operation = $this->getOperation('/api/test/{user}', 'get'); $operation = $this->getOperation('/api/test/{user}', 'get');
$this->assertEquals(['https'], $operation->getSchemes()); $this->assertEquals(['https'], $operation->security);
$this->assertEmpty($operation->getSummary()); $this->assertEquals(OA\UNDEFINED, $operation->summary);
$this->assertEmpty($operation->getDescription()); $this->assertEquals(OA\UNDEFINED, $operation->description);
$this->assertNull($operation->getDeprecated()); $this->assertEquals(OA\UNDEFINED, $operation->deprecated);
$this->assertTrue($operation->getResponses()->has(200)); $this->assertHasResponse(200, $operation);
$parameters = $operation->getParameters(); $this->assertHasParameter('user', 'path', $operation);
$this->assertTrue($parameters->has('user', 'path')); $parameter = Util::getOperationParameter($operation, 'user', 'path');
$this->assertTrue($parameter->required);
$parameter = $parameters->get('user', 'path'); $this->assertEquals('string', $parameter->schema->type);
$this->assertTrue($parameter->getRequired()); $this->assertEquals('/foo/', $parameter->schema->pattern);
$this->assertEquals('string', $parameter->getType()); $this->assertEquals(OA\UNDEFINED, $parameter->schema->format);
$this->assertEquals('/foo/', $parameter->getPattern());
$this->assertEmpty($parameter->getFormat());
} }
public function testDeprecatedAction() public function testDeprecatedAction()
{ {
$operation = $this->getOperation('/api/deprecated', 'get'); $operation = $this->getOperation('/api/deprecated', 'get');
$this->assertEquals('This action is deprecated.', $operation->getSummary()); $this->assertEquals('This action is deprecated.', $operation->summary);
$this->assertEquals('Please do not use this action.', $operation->getDescription()); $this->assertEquals('Please do not use this action.', $operation->description);
$this->assertTrue($operation->getDeprecated()); $this->assertTrue($operation->deprecated);
} }
public function testApiPlatform() public function testApiPlatform()
@ -160,6 +157,7 @@ class FunctionalTest extends WebTestCase
'readOnly' => true, 'readOnly' => true,
'title' => 'userid', 'title' => 'userid',
'example' => 1, 'example' => 1,
'default' => null,
], ],
'email' => [ 'email' => [
'type' => 'string', 'type' => 'string',
@ -182,15 +180,15 @@ class FunctionalTest extends WebTestCase
], ],
'users' => [ 'users' => [
'items' => [ 'items' => [
'$ref' => '#/definitions/User', '$ref' => '#/components/schemas/User',
], ],
'type' => 'array', 'type' => 'array',
], ],
'friend' => [ 'friend' => [
'$ref' => '#/definitions/User', '$ref' => '#/components/schemas/User',
], ],
'dummy' => [ 'dummy' => [
'$ref' => '#/definitions/Dummy2', '$ref' => '#/components/schemas/Dummy2',
], ],
'status' => [ 'status' => [
'type' => 'string', 'type' => 'string',
@ -201,8 +199,9 @@ class FunctionalTest extends WebTestCase
'format' => 'date-time', 'format' => 'date-time',
], ],
], ],
'schema' => 'User',
], ],
$this->getModel('User')->toArray() json_decode($this->getModel('User')->toJson(), true)
); );
} }
@ -215,13 +214,13 @@ class FunctionalTest extends WebTestCase
'items' => ['type' => 'string'], 'items' => ['type' => 'string'],
'type' => 'array', 'type' => 'array',
], ],
'dummy' => ['$ref' => '#/definitions/DummyType'], 'dummy' => ['$ref' => '#/components/schemas/DummyType'],
'dummies' => [ 'dummies' => [
'items' => ['$ref' => '#/definitions/DummyType'], 'items' => ['$ref' => '#/components/schemas/DummyType'],
'type' => 'array', 'type' => 'array',
], ],
'empty_dummies' => [ 'empty_dummies' => [
'items' => ['$ref' => '#/definitions/DummyEmptyType'], 'items' => ['$ref' => '#/components/schemas/DummyEmptyType'],
'type' => 'array', 'type' => 'array',
], ],
'quz' => [ 'quz' => [
@ -254,7 +253,8 @@ class FunctionalTest extends WebTestCase
], ],
], ],
'required' => ['dummy', 'dummies', 'entity', 'entities', 'document', 'documents', 'extended_builtin'], 'required' => ['dummy', 'dummies', 'entity', 'entities', 'document', 'documents', 'extended_builtin'],
], $this->getModel('UserType')->toArray()); 'schema' => 'UserType',
], json_decode($this->getModel('UserType')->toJson(), true));
$this->assertEquals([ $this->assertEquals([
'type' => 'object', 'type' => 'object',
@ -299,7 +299,8 @@ class FunctionalTest extends WebTestCase
], ],
], ],
'required' => ['foo', 'foz', 'password'], 'required' => ['foo', 'foz', 'password'],
], $this->getModel('DummyType')->toArray()); 'schema' => 'DummyType',
], json_decode($this->getModel('DummyType')->toJson(), true));
} }
public function testSecurityAction() public function testSecurityAction()
@ -310,7 +311,7 @@ class FunctionalTest extends WebTestCase
['api_key' => []], ['api_key' => []],
['basic' => []], ['basic' => []],
]; ];
$this->assertEquals($expected, $operation->getSecurity()); $this->assertEquals($expected, $operation->security);
} }
public function testClassSecurityAction() public function testClassSecurityAction()
@ -320,7 +321,7 @@ class FunctionalTest extends WebTestCase
$expected = [ $expected = [
['basic' => []], ['basic' => []],
]; ];
$this->assertEquals($expected, $operation->getSecurity()); $this->assertEquals($expected, $operation->security);
} }
public function testSymfonyConstraintDocumentation() public function testSymfonyConstraintDocumentation()
@ -382,29 +383,30 @@ class FunctionalTest extends WebTestCase
], ],
], ],
'type' => 'object', 'type' => 'object',
], $this->getModel('SymfonyConstraints')->toArray()); 'schema' => 'SymfonyConstraints',
], json_decode($this->getModel('SymfonyConstraints')->toJson(), true));
} }
public function testConfigReference() public function testConfigReference()
{ {
$operation = $this->getOperation('/api/configReference', 'get'); $operation = $this->getOperation('/api/configReference', 'get');
$this->assertEquals('#/definitions/Test', $operation->getResponses()->get('200')->getSchema()->getRef()); $this->assertEquals('#/components/schemas/Test', $this->getOperationResponse($operation, '200')->ref);
$this->assertEquals('#/responses/201', $operation->getResponses()->get('201')->getRef()); $this->assertEquals('#/components/responses/201', $this->getOperationResponse($operation, '201')->ref);
} }
public function testOperationsWithOtherAnnotationsAction() public function testOperationsWithOtherAnnotationsAction()
{ {
$getOperation = $this->getOperation('/api/multi-annotations', 'get'); $getOperation = $this->getOperation('/api/multi-annotations', 'get');
$this->assertSame('This is the get operation', $getOperation->getDescription()); $this->assertSame('This is the get operation', $getOperation->description);
$this->assertSame('Worked well!', $getOperation->getResponses()->get(200)->getDescription()); $this->assertSame('Worked well!', $this->getOperationResponse($getOperation, 200)->description);
$postOperation = $this->getOperation('/api/multi-annotations', 'post'); $postOperation = $this->getOperation('/api/multi-annotations', 'post');
$this->assertSame('This is post', $postOperation->getDescription()); $this->assertSame('This is post', $postOperation->description);
$this->assertSame('Worked well!', $postOperation->getResponses()->get(200)->getDescription()); $this->assertSame('Worked well!', $this->getOperationResponse($postOperation, 200)->description);
} }
public function testNoDuplicatedParameters() public function testNoDuplicatedParameters()
{ {
$this->assertFalse($this->getOperation('/api/article/{id}', 'get')->getParameters()->has('id', 'path')); $this->assertNotHasParameter('name', 'path', $this->getOperation('/api/article/{id}', 'get'));
} }
} }

View File

@ -29,7 +29,8 @@ class JMSFunctionalTest extends WebTestCase
'type' => 'integer', 'type' => 'integer',
], ],
], ],
], $this->getModel('JMSPicture')->toArray()); 'schema' => 'JMSPicture',
], json_decode($this->getModel('JMSPicture')->toJson(), true));
$this->assertEquals([ $this->assertEquals([
'type' => 'object', 'type' => 'object',
@ -38,7 +39,8 @@ class JMSFunctionalTest extends WebTestCase
'type' => 'integer', 'type' => 'integer',
], ],
], ],
], $this->getModel('JMSPicture_mini')->toArray()); 'schema' => 'JMSPicture_mini',
], json_decode($this->getModel('JMSPicture_mini')->toJson(), true));
} }
public function testModeChatDocumentation() public function testModeChatDocumentation()
@ -51,21 +53,23 @@ class JMSFunctionalTest extends WebTestCase
], ],
'members' => [ 'members' => [
'items' => [ 'items' => [
'$ref' => '#/definitions/JMSChatUser', '$ref' => '#/components/schemas/JMSChatUser',
], ],
'type' => 'array', 'type' => 'array',
], ],
], ],
], $this->getModel('JMSChat')->toArray()); 'schema' => 'JMSChat',
], json_decode($this->getModel('JMSChat')->toJson(), true));
$this->assertEquals([ $this->assertEquals([
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'picture' => [ 'picture' => [
'$ref' => '#/definitions/JMSPicture', '$ref' => '#/components/schemas/JMSPicture',
], ],
], ],
], $this->getModel('JMSChatUser')->toArray()); 'schema' => 'JMSChatUser',
], json_decode($this->getModel('JMSChatUser')->toJson(), true));
} }
public function testModelDocumentation() public function testModelDocumentation()
@ -79,6 +83,7 @@ class JMSFunctionalTest extends WebTestCase
'readOnly' => true, 'readOnly' => true,
'title' => 'userid', 'title' => 'userid',
'example' => 1, 'example' => 1,
'default' => null,
], ],
'daysOnline' => [ 'daysOnline' => [
'type' => 'integer', 'type' => 'integer',
@ -106,13 +111,13 @@ class JMSFunctionalTest extends WebTestCase
'friends' => [ 'friends' => [
'type' => 'array', 'type' => 'array',
'items' => [ 'items' => [
'$ref' => '#/definitions/User', '$ref' => '#/components/schemas/User',
], ],
], ],
'indexed_friends' => [ 'indexed_friends' => [
'type' => 'object', 'type' => 'object',
'additionalProperties' => [ 'additionalProperties' => [
'$ref' => '#/definitions/User', '$ref' => '#/components/schemas/User',
], ],
], ],
'favorite_dates' => [ 'favorite_dates' => [
@ -127,7 +132,7 @@ class JMSFunctionalTest extends WebTestCase
'format' => 'date-time', 'format' => 'date-time',
], ],
'best_friend' => [ 'best_friend' => [
'$ref' => '#/definitions/User', '$ref' => '#/components/schemas/User',
], ],
'status' => [ 'status' => [
'type' => 'string', 'type' => 'string',
@ -136,10 +141,12 @@ class JMSFunctionalTest extends WebTestCase
'enum' => ['disabled', 'enabled'], 'enum' => ['disabled', 'enabled'],
], ],
'virtual_type1' => [ 'virtual_type1' => [
'$ref' => '#/definitions/VirtualTypeClassDoesNotExistsHandlerDefined', 'title' => 'JMS custom types handled via Custom Type Handlers.',
'$ref' => '#/components/schemas/VirtualTypeClassDoesNotExistsHandlerDefined',
], ],
'virtual_type2' => [ 'virtual_type2' => [
'$ref' => '#/definitions/VirtualTypeClassDoesNotExistsHandlerNotDefined', 'title' => 'JMS custom types handled via Custom Type Handlers.',
'$ref' => '#/components/schemas/VirtualTypeClassDoesNotExistsHandlerNotDefined',
], ],
'last_update' => [ 'last_update' => [
'type' => 'date', 'type' => 'date',
@ -199,10 +206,12 @@ class JMSFunctionalTest extends WebTestCase
'type' => 'integer', 'type' => 'integer',
], ],
], ],
], $this->getModel('JMSUser')->toArray()); 'schema' => 'JMSUser',
], json_decode($this->getModel('JMSUser')->toJson(), true));
$this->assertEquals([ $this->assertEquals([
], $this->getModel('VirtualTypeClassDoesNotExistsHandlerNotDefined')->toArray()); 'schema' => 'VirtualTypeClassDoesNotExistsHandlerNotDefined',
], json_decode($this->getModel('VirtualTypeClassDoesNotExistsHandlerNotDefined')->toJson(), true));
$this->assertEquals([ $this->assertEquals([
'type' => 'object', 'type' => 'object',
@ -211,7 +220,8 @@ class JMSFunctionalTest extends WebTestCase
'type' => 'string', 'type' => 'string',
], ],
], ],
], $this->getModel('VirtualTypeClassDoesNotExistsHandlerDefined')->toArray()); 'schema' => 'VirtualTypeClassDoesNotExistsHandlerDefined',
], json_decode($this->getModel('VirtualTypeClassDoesNotExistsHandlerDefined')->toJson(), true));
} }
public function testModelComplexDualDocumentation() public function testModelComplexDualDocumentation()
@ -223,13 +233,14 @@ class JMSFunctionalTest extends WebTestCase
'type' => 'integer', 'type' => 'integer',
], ],
'complex' => [ 'complex' => [
'$ref' => '#/definitions/JMSComplex2', '$ref' => '#/components/schemas/JMSComplex2',
], ],
'user' => [ 'user' => [
'$ref' => '#/definitions/JMSUser', '$ref' => '#/components/schemas/JMSUser',
], ],
], ],
], $this->getModel('JMSDualComplex')->toArray()); 'schema' => 'JMSDualComplex',
], json_decode($this->getModel('JMSDualComplex')->toJson(), true));
} }
public function testNestedGroups() public function testNestedGroups()
@ -237,10 +248,11 @@ class JMSFunctionalTest extends WebTestCase
$this->assertEquals([ $this->assertEquals([
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'living' => ['$ref' => '#/definitions/JMSChatLivingRoom'], 'living' => ['$ref' => '#/components/schemas/JMSChatLivingRoom'],
'dining' => ['$ref' => '#/definitions/JMSChatRoom'], 'dining' => ['$ref' => '#/components/schemas/JMSChatRoom'],
], ],
], $this->getModel('JMSChatFriend')->toArray()); 'schema' => 'JMSChatFriend',
], json_decode($this->getModel('JMSChatFriend')->toJson(), true));
$this->assertEquals([ $this->assertEquals([
'type' => 'object', 'type' => 'object',
@ -248,7 +260,8 @@ class JMSFunctionalTest extends WebTestCase
'id1' => ['type' => 'integer'], 'id1' => ['type' => 'integer'],
'id3' => ['type' => 'integer'], 'id3' => ['type' => 'integer'],
], ],
], $this->getModel('JMSChatRoom')->toArray()); 'schema' => 'JMSChatRoom',
], json_decode($this->getModel('JMSChatRoom')->toJson(), true));
} }
public function testModelComplexDocumentation() public function testModelComplexDocumentation()
@ -257,15 +270,16 @@ class JMSFunctionalTest extends WebTestCase
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'id' => ['type' => 'integer'], 'id' => ['type' => 'integer'],
'user' => ['$ref' => '#/definitions/JMSUser'], 'user' => ['$ref' => '#/components/schemas/JMSUser'],
'name' => ['type' => 'string'], 'name' => ['type' => 'string'],
'virtual' => ['$ref' => '#/definitions/JMSUser'], 'virtual' => ['$ref' => '#/components/schemas/JMSUser'],
], ],
'required' => [ 'required' => [
'id', 'id',
'user', 'user',
], ],
], $this->getModel('JMSComplex')->toArray()); 'schema' => 'JMSComplex',
], json_decode($this->getModel('JMSComplex')->toJson(), true));
} }
public function testYamlConfig() public function testYamlConfig()
@ -280,7 +294,8 @@ class JMSFunctionalTest extends WebTestCase
'type' => 'string', 'type' => 'string',
], ],
], ],
], $this->getModel('VirtualProperty')->toArray()); 'schema' => 'VirtualProperty',
], json_decode($this->getModel('VirtualProperty')->toJson(), true));
} }
public function testNamingStrategyWithConstraints() public function testNamingStrategyWithConstraints()
@ -295,7 +310,8 @@ class JMSFunctionalTest extends WebTestCase
], ],
], ],
'required' => ['beautifulName'], 'required' => ['beautifulName'],
], $this->getModel('JMSNamingStrategyConstraints')->toArray()); 'schema' => 'JMSNamingStrategyConstraints',
], json_decode($this->getModel('JMSNamingStrategyConstraints')->toJson(), true));
} }
protected static function createKernel(array $options = []) protected static function createKernel(array $options = [])

View File

@ -11,17 +11,19 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber; namespace Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface; use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
class VirtualTypeClassDoesNotExistsHandlerDefinedDescriber implements ModelDescriberInterface class VirtualTypeClassDoesNotExistsHandlerDefinedDescriber implements ModelDescriberInterface
{ {
public function describe(Model $model, Schema $schema) public function describe(Model $model, OA\Schema $schema)
{ {
$schema->setType('object'); $schema->type = 'object';
$schema->getProperties()->get('custom_prop')->setType('string'); $property = Util::getProperty($schema, 'custom_prop');
$property->type = 'string';
} }
public function supports(Model $model): bool public function supports(Model $model): bool

View File

@ -35,8 +35,7 @@ class SwaggerUiTest extends WebTestCase
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('text/html; charset=UTF-8', $response->headers->get('Content-Type')); $this->assertEquals('text/html; charset=UTF-8', $response->headers->get('Content-Type'));
$expected = $this->getSwaggerDefinition()->toArray(); $expected = json_decode($this->getOpenApiDefinition()->toJson(), true);
$expected['basePath'] = '/app_dev.php';
$this->assertEquals($expected, json_decode($crawler->filterXPath('//script[@id="swagger-data"]')->text(), true)['spec']); $this->assertEquals($expected, json_decode($crawler->filterXPath('//script[@id="swagger-data"]')->text(), true)['spec']);
} }
@ -49,28 +48,10 @@ class SwaggerUiTest extends WebTestCase
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('text/html; charset=UTF-8', $response->headers->get('Content-Type')); $this->assertEquals('text/html; charset=UTF-8', $response->headers->get('Content-Type'));
$expected = $this->getSwaggerDefinition()->toArray(); $expected = json_decode($this->getOpenApiDefinition('test')->toJson(), true);
$expected['basePath'] = '/app_dev.php'; $expected['servers'] = [
$expected['info']['title'] = 'My Test App'; ['url' => 'http://api.example.com/app_dev.php'],
$expected['paths'] = [
'/api/dummies' => $expected['paths']['/api/dummies'],
'/api/foo' => $expected['paths']['/api/foo'],
'/api/dummies/{id}' => $expected['paths']['/api/dummies/{id}'],
'/test/test/' => ['get' => [
'responses' => ['200' => ['description' => 'Test']],
]],
'/test/test/{id}' => ['get' => [
'responses' => ['200' => ['description' => 'Test Ref']],
'parameters' => [['$ref' => '#/parameters/test']],
]],
]; ];
$expected['definitions'] = [
'Dummy' => $expected['definitions']['Dummy'],
'Test' => ['type' => 'string'],
'JMSPicture_mini' => ['type' => 'object'],
'BazingaUser_grouped' => ['type' => 'object'],
];
$this->assertEquals($expected, json_decode($crawler->filterXPath('//script[@id="swagger-data"]')->text(), true)['spec']); $this->assertEquals($expected, json_decode($crawler->filterXPath('//script[@id="swagger-data"]')->text(), true)['spec']);
} }
@ -82,9 +63,10 @@ class SwaggerUiTest extends WebTestCase
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('application/json', $response->headers->get('Content-Type')); $this->assertEquals('application/json', $response->headers->get('Content-Type'));
$expected = $this->getSwaggerDefinition()->toArray(); $expected = json_decode($this->getOpenApiDefinition()->toJson(), true);
$expected['basePath'] = '/app_dev.php'; $expected['servers'] = [
$expected['host'] = 'api.example.com'; ['url' => 'http://api.example.com/app_dev.php'],
];
$this->assertEquals($expected, json_decode($response->getContent(), true)); $this->assertEquals($expected, json_decode($response->getContent(), true));
} }

View File

@ -160,21 +160,23 @@ class TestKernel extends Kernel
'info' => [ 'info' => [
'title' => 'My Default App', 'title' => 'My Default App',
], ],
'definitions' => [ 'components' => [
'Test' => [ 'schemas' => [
'type' => 'string', 'Test' => [
'type' => 'string',
],
], ],
], 'parameters' => [
'parameters' => [ 'test' => [
'test' => [ 'name' => 'id',
'name' => 'id', 'in' => 'path',
'in' => 'path', 'required' => true,
'required' => true, ],
], ],
], 'responses' => [
'responses' => [ '201' => [
'201' => [ 'description' => 'Awesome description',
'description' => 'Awesome description', ],
], ],
], ],
], ],

View File

@ -11,8 +11,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
use EXSyst\Component\Swagger\Operation; use OpenApi\Annotations as OA;
use EXSyst\Component\Swagger\Schema;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase;
class WebTestCase extends BaseWebTestCase class WebTestCase extends BaseWebTestCase
@ -22,29 +21,139 @@ class WebTestCase extends BaseWebTestCase
return new TestKernel(); return new TestKernel();
} }
protected function getSwaggerDefinition($area = 'default') protected function getOpenApiDefinition($area = 'default'): OA\OpenApi
{ {
return static::$kernel->getContainer()->get(sprintf('nelmio_api_doc.generator.%s', $area))->generate(); return static::$kernel->getContainer()->get(sprintf('nelmio_api_doc.generator.%s', $area))->generate();
} }
protected function getModel($name): Schema protected function getModel($name): OA\Schema
{ {
$definitions = $this->getSwaggerDefinition()->getDefinitions(); $api = $this->getOpenApiDefinition();
$this->assertTrue($definitions->has($name)); $key = array_search($name, array_column($api->components->schemas, 'schema'));
static::assertNotFalse($key, sprintf('Model "%s" does not exist.', $name));
return $definitions->get($name); return $api->components->schemas[$key];
} }
protected function getOperation($path, $method): Operation protected function getOperation($path, $method): OA\Operation
{ {
$api = $this->getSwaggerDefinition(); $path = $this->getPath($path);
$paths = $api->getPaths();
$this->assertTrue($paths->has($path), sprintf('Path "%s" does not exist.', $path)); $this->assertInstanceOf(
$action = $paths->get($path); OA\Operation::class,
$path->{$method},
sprintf('Operation "%s" for path "%s" does not exist', $method, $path->path)
);
$this->assertTrue($action->hasOperation($method), sprintf('Operation "%s" for path "%s" does not exist', $path, $method)); return $path->{$method};
}
return $action->getOperation($method); protected function getOperationResponse(OA\Operation $operation, $response): OA\Response
{
$this->assertHasResponse($response, $operation);
$key = array_search($response, array_column($operation->responses, 'response'));
return $operation->responses[$key];
}
protected function getProperty(OA\Schema $annotation, $property): OA\Property
{
$this->assertHasProperty($property, $annotation);
$key = array_search($property, array_column($annotation->properties, 'property'));
return $annotation->properties[$key];
}
protected function getParameter(OA\AbstractAnnotation $annotation, $name, $in): OA\Parameter
{
/* @var OA\Operation|OA\OpenApi $annotation */
$this->assertHasParameter($name, $in, $annotation);
$parameters = array_filter($annotation->parameters ?: [], function (OA\Parameter $parameter) use ($name, $in) {
return $parameter->name === $name && $parameter->in === $in;
});
return array_values($parameters)[0];
}
protected function getPath($path): OA\PathItem
{
$api = $this->getOpenApiDefinition();
self::assertHasPath($path, $api);
return $api->paths[array_search($path, array_column($api->paths, 'path'))];
}
public function assertHasPath($path, OA\OpenApi $api)
{
$paths = array_column(OA\UNDEFINED !== $api->paths ? $api->paths : [], 'path');
static::assertContains(
$path,
$paths,
sprintf('Failed asserting that path "%s" does exist.', $path)
);
}
public function assertNotHasPath($path, OA\OpenApi $api)
{
$paths = array_column(OA\UNDEFINED !== $api->paths ? $api->paths : [], 'path');
static::assertNotContains(
$path,
$paths,
sprintf('Failed asserting that path "%s" does not exist.', $path)
);
}
public function assertHasResponse($responseCode, OA\Operation $operation)
{
$responses = array_column(OA\UNDEFINED !== $operation->responses ? $operation->responses : [], 'response');
static::assertContains(
$responseCode,
$responses,
sprintf('Failed asserting that response "%s" does exist.', $responseCode)
);
}
public function assertHasParameter($name, $in, OA\AbstractAnnotation $annotation)
{
/* @var OA\Operation|OA\OpenApi $annotation */
$parameters = array_column(OA\UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], 'name', 'in');
static::assertContains(
$name,
$parameters[$in] ?? [],
sprintf('Failed asserting that parameter "%s" in "%s" does exist.', $name, $in)
);
}
public function assertNotHasParameter($name, $in, OA\AbstractAnnotation $annotation)
{
/* @var OA\Operation|OA\OpenApi $annotation */
$parameters = array_column(OA\UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], 'name', 'in');
static::assertNotContains(
$name,
$parameters[$in] ?? [],
sprintf('Failed asserting that parameter "%s" in "%s" does not exist.', $name, $in)
);
}
public function assertHasProperty($property, OA\AbstractAnnotation $annotation)
{
/* @var OA\Schema|OA\Property|OA\Items $annotation */
$properties = array_column(OA\UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property');
static::assertContains(
$property,
$properties,
sprintf('Failed asserting that property "%s" does exist.', $property)
);
}
public function assertNotHasProperty($property, OA\AbstractAnnotation $annotation)
{
/* @var OA\Schema|OA\Property|OA\Items $annotation */
$properties = array_column(OA\UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property');
static::assertNotContains(
$property,
$properties,
sprintf('Failed asserting that property "%s" does not exist.', $property)
);
} }
} }

View File

@ -11,9 +11,9 @@
namespace Nelmio\ApiDocBundle\Tests\Model; namespace Nelmio\ApiDocBundle\Tests\Model;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\Model\ModelRegistry;
use OpenApi\Annotations as OA;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
@ -27,10 +27,10 @@ class ModelRegistryTest extends TestCase
'groups' => ['group1'], 'groups' => ['group1'],
], ],
]; ];
$registry = new ModelRegistry([], new Swagger(), $alternativeNames); $registry = new ModelRegistry([], new OA\OpenApi([]), $alternativeNames);
$type = new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true); $type = new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true);
$this->assertEquals('#/definitions/array', $registry->register(new Model($type, ['group1']))); $this->assertEquals('#/components/schemas/array', $registry->register(new Model($type, ['group1'])));
} }
/** /**
@ -40,7 +40,7 @@ class ModelRegistryTest extends TestCase
*/ */
public function testNameAliasingForObjects(string $expected, $groups, array $alternativeNames) public function testNameAliasingForObjects(string $expected, $groups, array $alternativeNames)
{ {
$registry = new ModelRegistry([], new Swagger(), $alternativeNames); $registry = new ModelRegistry([], new OA\OpenApi([]), $alternativeNames);
$type = new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class); $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class);
$this->assertEquals($expected, $registry->register(new Model($type, $groups))); $this->assertEquals($expected, $registry->register(new Model($type, $groups)));
@ -50,7 +50,7 @@ class ModelRegistryTest extends TestCase
{ {
return [ return [
[ [
'#/definitions/ModelRegistryTest', '#/components/schemas/ModelRegistryTest',
null, null,
[ [
'Foo1' => [ 'Foo1' => [
@ -60,7 +60,7 @@ class ModelRegistryTest extends TestCase
], ],
], ],
[ [
'#/definitions/Foo1', '#/components/schemas/Foo1',
['group1'], ['group1'],
[ [
'Foo1' => [ 'Foo1' => [
@ -70,7 +70,7 @@ class ModelRegistryTest extends TestCase
], ],
], ],
[ [
'#/definitions/Foo1', '#/components/schemas/Foo1',
['group1', 'group2'], ['group1', 'group2'],
[ [
'Foo1' => [ 'Foo1' => [
@ -80,7 +80,7 @@ class ModelRegistryTest extends TestCase
], ],
], ],
[ [
'#/definitions/ModelRegistryTest', '#/components/schemas/ModelRegistryTest',
null, null,
[ [
'Foo1' => [ 'Foo1' => [
@ -90,7 +90,7 @@ class ModelRegistryTest extends TestCase
], ],
], ],
[ [
'#/definitions/Foo1', '#/components/schemas/Foo1',
[], [],
[ [
'Foo1' => [ 'Foo1' => [
@ -110,9 +110,9 @@ class ModelRegistryTest extends TestCase
$this->expectException('\LogicException'); $this->expectException('\LogicException');
$this->expectExceptionMessage(sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $stringType)); $this->expectExceptionMessage(sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $stringType));
$registry = new ModelRegistry([], new Swagger()); $registry = new ModelRegistry([], new OA\OpenApi([]));
$registry->register(new Model($type)); $registry->register(new Model($type));
$registry->registerDefinitions(); $registry->registerSchemas();
} }
public function unsupportedTypesProvider() public function unsupportedTypesProvider()

View File

@ -12,8 +12,8 @@
namespace Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations; namespace Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationReader;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\SymfonyConstraintAnnotationReader; use Nelmio\ApiDocBundle\ModelDescriber\Annotations\SymfonyConstraintAnnotationReader;
use OpenApi\Annotations as OA;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@ -33,18 +33,18 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
private $property2; private $property2;
}; };
$schema = new Schema(); $schema = new OA\Schema([]);
$schema->getProperties()->set('property1', new Schema()); $schema->merge([new OA\Property(['property' => 'property1'])]);
$schema->getProperties()->set('property2', new Schema()); $schema->merge([new OA\Property(['property' => 'property2'])]);
$symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader());
$symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->setSchema($schema);
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->getProperties()->get('property1')); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property2'), $schema->getProperties()->get('property2')); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property2'), $schema->properties[1]);
// expect required to be numeric array with sequential keys (not [0 => ..., 2 => ...]) // expect required to be numeric array with sequential keys (not [0 => ..., 2 => ...])
$this->assertEquals($schema->getRequired(), ['property1', 'property2']); $this->assertEquals($schema->required, ['property1', 'property2']);
} }
public function testAssertChoiceResultsInNumericArray() public function testAssertChoiceResultsInNumericArray()
@ -62,15 +62,15 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
private $property1; private $property1;
}; };
$schema = new Schema(); $schema = new OA\Schema([]);
$schema->getProperties()->set('property1', new Schema()); $schema->merge([new OA\Property(['property' => 'property1'])]);
$symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader());
$symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->setSchema($schema);
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->getProperties()->get('property1')); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
// expect enum to be numeric array with sequential keys (not [1 => "active", 2 => "active"]) // expect enum to be numeric array with sequential keys (not [1 => "active", 2 => "active"])
$this->assertEquals($schema->getProperties()->get('property1')->getEnum(), ['active', 'blocked']); $this->assertEquals($schema->properties[0]->enum, ['active', 'blocked']);
} }
} }

View File

@ -1,67 +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\Tests\Swagger;
use Doctrine\Common\Annotations\AnnotationReader;
use EXSyst\Component\Swagger\Schema;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Annotation\Model as ModelAnnotation;
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
use Nelmio\ApiDocBundle\SwaggerPhp\ModelRegister;
use PHPUnit\Framework\TestCase;
use Swagger\Analysis;
use Swagger\Annotations as SWG;
class ModelRegisterTest extends TestCase
{
/**
* @group legacy
* @expectedDeprecation Using `@Model` implicitly in a `@SWG\Schema`, `@SWG\Items` or `@SWG\Property` annotation in %s. Use `ref=@Model()` instead.
*/
public function testDeprecatedImplicitUseOfModel()
{
$api = new Swagger();
$registry = new ModelRegistry([new NullModelDescriber()], $api);
$modelRegister = new ModelRegister($registry);
$annotationsReader = new AnnotationReader();
$modelRegister->__invoke(new Analysis([$annotation = $annotationsReader->getPropertyAnnotation(
new \ReflectionProperty(Foo::class, 'bar'),
SWG\Property::class
)]));
$this->assertEquals(['items' => ['$ref' => '#/definitions/Foo']], json_decode(json_encode($annotation), true));
}
}
class Foo
{
/**
* @SWG\Property(@ModelAnnotation(type=Foo::class))
*/
private $bar;
}
class NullModelDescriber implements ModelDescriberInterface
{
public function describe(Model $model, Schema $schema)
{
}
public function supports(Model $model): bool
{
return true;
}
}

View File

@ -0,0 +1,842 @@
<?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\Tests\SwaggerPhp;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use OpenApi\Context;
use const OpenApi\UNDEFINED;
use PHPUnit\Framework\TestCase;
/**
* Class UtilTest.
*
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getOperation
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getOperationParameter
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getChild
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getCollectionItem
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getIndexedCollectionItem
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::searchCollectionItem
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::searchIndexedCollectionItem
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::createCollectionItem
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::createChild
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::createContext
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::merge
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeFromArray
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeChild
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeCollection
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeTyped
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeProperty
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getNestingIndexes
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getNesting
*/
class UtilTest extends TestCase
{
public $rootContext;
/** @var OA\OpenApi */
public $rootAnnotation;
public function setUp()
{
parent::setUp();
$this->rootContext = new Context(['isTestingRoot' => true]);
$this->rootAnnotation = new OA\OpenApi(['_context' => $this->rootContext]);
}
public function testCreateContextSetsParentContext()
{
$context = Util::createContext([], $this->rootContext);
$this->assertSame($this->rootContext, $context->getRootContext());
}
public function testCreateContextWithProperties()
{
$context = Util::createContext(['testing' => 'trait']);
$this->assertTrue($context->is('testing'));
$this->assertSame('trait', $context->testing);
}
public function testCreateChild()
{
$info = Util::createChild($this->rootAnnotation, OA\Info::class);
$this->assertInstanceOf(OA\Info::class, $info);
}
public function testCreateChildHasContext()
{
$info = Util::createChild($this->rootAnnotation, OA\Info::class);
$this->assertInstanceOf(Context::class, $info->_context);
}
public function testCreateChildHasNestedContext()
{
$path = Util::createChild($this->rootAnnotation, OA\PathItem::class);
$this->assertIsNested($this->rootAnnotation, $path);
$parameter = Util::createChild($path, OA\Parameter::class);
$this->assertIsNested($path, $parameter);
$schema = Util::createChild($parameter, OA\Schema::class);
$this->assertIsNested($parameter, $schema);
$this->assertIsConnectedToRootContext($schema);
}
public function testCreateChildWithEmptyProperties()
{
$properties = [];
/** @var OA\Info $info */
$info = Util::createChild($this->rootAnnotation, OA\Info::class, $properties);
$properties = array_filter(get_object_vars($info), function ($key) {
return 0 !== strpos($key, '_');
}, ARRAY_FILTER_USE_KEY);
$this->assertEquals([UNDEFINED], array_unique(array_values($properties)));
$this->assertIsNested($this->rootAnnotation, $info);
$this->assertIsConnectedToRootContext($info);
}
public function testCreateChildWithProperties()
{
$properties = ['title' => 'testing', 'version' => '999', 'x' => new \stdClass()];
/** @var OA\Info $info */
$info = Util::createChild($this->rootAnnotation, OA\Info::class, $properties);
$this->assertSame($info->title, $properties['title']);
$this->assertSame($info->version, $properties['version']);
$this->assertSame($info->x, $properties['x']);
$this->assertIsNested($this->rootAnnotation, $info);
$this->assertIsConnectedToRootContext($info);
}
public function testCreateCollectionItemAddsCreatedItemToCollection()
{
$collection = 'paths';
$class = OA\PathItem::class;
$p1 = Util::createCollectionItem($this->rootAnnotation, $collection, $class);
$this->assertSame(0, $p1);
$this->assertCount(1, $this->rootAnnotation->{$collection});
$this->assertInstanceOf($class, $this->rootAnnotation->{$collection}[$p1]);
$this->assertIsNested($this->rootAnnotation, $this->rootAnnotation->{$collection}[$p1]);
$this->assertIsConnectedToRootContext($this->rootAnnotation->{$collection}[$p1]);
$p2 = Util::createCollectionItem($this->rootAnnotation, $collection, $class);
$this->assertSame(1, $p2);
$this->assertCount(2, $this->rootAnnotation->{$collection});
$this->assertInstanceOf($class, $this->rootAnnotation->{$collection}[$p2]);
$this->assertIsNested($this->rootAnnotation, $this->rootAnnotation->{$collection}[$p2]);
$this->assertIsConnectedToRootContext($this->rootAnnotation->{$collection}[$p2]);
$this->rootAnnotation->components = Util::createChild($this->rootAnnotation, OA\Components::class);
$collection = 'schemas';
$class = OA\Schema::class;
$d1 = Util::createCollectionItem($this->rootAnnotation->components, $collection, $class);
$this->assertSame(0, $d1);
$this->assertCount(1, $this->rootAnnotation->components->{$collection});
$this->assertInstanceOf($class, $this->rootAnnotation->components->{$collection}[$d1]);
$this->assertIsNested($this->rootAnnotation->components, $this->rootAnnotation->components->{$collection}[$d1]);
$this->assertIsConnectedToRootContext($this->rootAnnotation->components->{$collection}[$d1]);
}
public function testCreateCollectionItemDoesNotAddToUnknownProperty()
{
$collection = 'foobars';
$class = OA\Info::class;
$expectedRegex = "/Property \"{$collection}\" doesn't exist .*/";
set_error_handler(function ($_, $err) { echo $err; });
$this->expectOutputRegex($expectedRegex);
Util::createCollectionItem($this->rootAnnotation, $collection, $class);
$this->expectOutputRegex($expectedRegex);
$this->assertNull($this->rootAnnotation->{$collection});
restore_error_handler();
}
public function testSearchCollectionItem()
{
$item1 = new \stdClass();
$item1->prop1 = 'item 1 prop 1';
$item1->prop2 = 'item 1 prop 2';
$item1->prop3 = 'item 1 prop 3';
$item2 = new \stdClass();
$item2->prop1 = 'item 2 prop 1';
$item2->prop2 = 'item 2 prop 2';
$item2->prop3 = 'item 2 prop 3';
$collection = [
$item1,
$item2,
];
$this->assertSame(0, Util::searchCollectionItem($collection, get_object_vars($item1)));
$this->assertSame(1, Util::searchCollectionItem($collection, get_object_vars($item2)));
$this->assertNull(Util::searchCollectionItem(
$collection,
array_merge(get_object_vars($item2), ['prop3' => 'foobar'])
));
$search = ['baz' => 'foobar'];
$this->expectOutputString('Undefined property: stdClass::$baz');
try {
Util::searchCollectionItem($collection, array_merge(get_object_vars($item2), $search));
} catch (\Exception $e) {
echo $e->getMessage();
}
// no exception on empty collection
$this->assertNull(Util::searchCollectionItem([], get_object_vars($item2)));
}
/**
* @dataProvider provideIndexedCollectionData
*/
public function testSearchIndexedCollectionItem($setup, $asserts)
{
foreach ($asserts as $collection => $items) {
foreach ($items as $assert) {
$setupCollection = empty($assert['components']) ?
($setup[$collection] ?? []) :
(OA\UNDEFINED !== $setup['components']->{$collection} ? $setup['components']->{$collection} : []);
// get the indexing correct within haystack preparation
$properties = array_fill(0, \count($setupCollection), null);
// prepare the haystack array
foreach ($items as $assertItem) {
// e.g. $properties[1] = new OA\PathItem(['path' => 'path 1'])
$properties[$assertItem['index']] = new $assertItem['class']([
$assertItem['key'] => $assertItem['value'],
]);
}
$this->assertSame(
$assert['index'],
Util::searchIndexedCollectionItem($properties, $assert['key'], $assert['value']),
sprintf('Failed to get the correct index for %s', print_r($assert, true))
);
}
}
}
/**
* @dataProvider provideIndexedCollectionData
*/
public function testGetIndexedCollectionItem($setup, $asserts)
{
$parent = new $setup['class'](array_merge(
$this->getSetupPropertiesWithoutClass($setup),
['_context' => $this->rootContext]
));
foreach ($asserts as $collection => $items) {
foreach ($items as $assert) {
$itemParent = empty($assert['components']) ? $parent : $parent->components;
$child = Util::getIndexedCollectionItem(
$itemParent,
$assert['class'],
$assert['value']
);
$this->assertInstanceOf($assert['class'], $child);
$this->assertSame($child->{$assert['key']}, $assert['value']);
$this->assertSame(
$itemParent->{$collection}[$assert['index']],
$child
);
$setupHaystack = empty($assert['components']) ?
$setup[$collection] ?? [] :
$setup['components']->{$collection} ?? [];
// the children created within provider are not connected
if (!\in_array($child, $setupHaystack, true)) {
$this->assertIsNested($itemParent, $child);
$this->assertIsConnectedToRootContext($child);
}
}
}
}
public function provideIndexedCollectionData(): array
{
return [[
'setup' => [
'class' => OA\OpenApi::class,
'paths' => [
new OA\PathItem(['path' => 'path 0']),
],
'components' => new OA\Components([
'parameters' => [
new OA\Parameter(['parameter' => 'parameter 0']),
new OA\Parameter(['parameter' => 'parameter 1']),
],
]),
],
'assert' => [
// one fixed within setup and one dynamically created
'paths' => [
[
'index' => 0,
'class' => OA\PathItem::class,
'key' => 'path',
'value' => 'path 0',
],
[
'index' => 1,
'class' => OA\PathItem::class,
'key' => 'path',
'value' => 'path 1',
],
],
// search indexes out of order followed by dynamically created
'parameters' => [
[
'index' => 1,
'class' => OA\Parameter::class,
'key' => 'parameter',
'value' => 'parameter 1',
'components' => true,
],
[
'index' => 0,
'class' => OA\Parameter::class,
'key' => 'parameter',
'value' => 'parameter 0',
'components' => true,
],
[
'index' => 2,
'class' => OA\Parameter::class,
'key' => 'parameter',
'value' => 'parameter 2',
'components' => true,
],
],
// two dynamically created
'responses' => [
[
'index' => 0,
'class' => OA\Response::class,
'key' => 'response',
'value' => 'response 0',
'components' => true,
],
[
'index' => 1,
'class' => OA\Response::class,
'key' => 'response',
'value' => 'response 1',
'components' => true,
],
],
// for sake of completeness
'securitySchemes' => [
[
'index' => 0,
'class' => OA\SecurityScheme::class,
'key' => 'securityScheme',
'value' => 'securityScheme 0',
'components' => true,
],
],
],
]];
}
/**
* @dataProvider provideChildData
*/
public function testGetChild($setup, $asserts)
{
$parent = new $setup['class'](array_merge(
$this->getSetupPropertiesWithoutClass($setup),
['_context' => $this->rootContext]
));
foreach ($asserts as $key => $assert) {
if (array_key_exists('exceptionMessage', $assert)) {
$this->expectExceptionMessage($assert['exceptionMessage']);
}
$child = Util::getChild($parent, $assert['class'], $assert['props']);
$this->assertInstanceOf($assert['class'], $child);
$this->assertSame($child, $parent->{$key});
if (\array_key_exists($key, $setup)) {
$this->assertSame($setup[$key], $parent->{$key});
}
$this->assertEquals($assert['props'], $this->getNonDefaultProperties($child));
}
}
public function provideChildData(): array
{
return [[
'setup' => [
'class' => OA\PathItem::class,
'get' => new OA\Get([]),
],
'assert' => [
// fixed within setup
'get' => [
'class' => OA\Get::class,
'props' => [],
],
// create new without props
'put' => [
'class' => OA\Put::class,
'props' => [],
],
// create new with multiple props
'delete' => [
'class' => OA\Delete::class,
'props' => [
'summary' => 'testing delete',
'deprecated' => true,
],
],
],
], [
'setup' => [
'class' => OA\Parameter::class,
],
'assert' => [
// create new with multiple props
'schema' => [
'class' => OA\Schema::class,
'props' => [
'ref' => '#/testing/schema',
'minProperties' => 0,
'enum' => [null, 'check', 999, false],
],
],
],
], [
'setup' => [
'class' => OA\Parameter::class,
],
'assert' => [
// externalDocs triggers invalid argument exception
'schema' => [
'class' => OA\Schema::class,
'props' => [
'externalDocs' => [],
],
'exceptionMessage' => 'Nesting Annotations is not supported.',
],
],
]];
}
public function testGetOperationParameterReturnsExisting()
{
$name = 'operation name';
$in = 'operation in';
$parameter = new OA\Parameter(['name' => $name, 'in' => $in]);
$operation = new OA\Get(['parameters' => [
new OA\Parameter([]),
new OA\Parameter(['name' => 'foo']),
new OA\Parameter(['in' => 'bar']),
new OA\Parameter(['name' => $name, 'in' => 'bar']),
new OA\Parameter(['name' => 'foo', 'in' => $in]),
$parameter,
]]);
$actual = Util::getOperationParameter($operation, $name, $in);
$this->assertSame($parameter, $actual);
}
public function testGetOperationParameterCreatesWithNameAndIn()
{
$name = 'operation name';
$in = 'operation in';
$operation = new OA\Get(['parameters' => [
new OA\Parameter([]),
new OA\Parameter(['name' => 'foo']),
new OA\Parameter(['in' => 'bar']),
new OA\Parameter(['name' => $name, 'in' => 'bar']),
new OA\Parameter(['name' => 'foo', 'in' => $in]),
]]);
$actual = Util::getOperationParameter($operation, $name, $in);
$this->assertInstanceOf(OA\Parameter::class, $actual);
$this->assertSame($name, $actual->name);
$this->assertSame($in, $actual->in);
}
public function testGetOperationReturnsExisting()
{
$get = new OA\Get([]);
$path = new OA\PathItem(['get' => $get]);
$this->assertSame($get, Util::getOperation($path, 'get'));
}
public function testGetOperationCreatesWithPath()
{
$pathStr = '/testing/get/path';
$path = new OA\PathItem(['path' => $pathStr]);
$get = Util::getOperation($path, 'get');
$this->assertInstanceOf(OA\Get::class, $get);
$this->assertSame($pathStr, $get->path);
}
public function testMergeWithEmptyArray()
{
$api = new OA\OpenApi([]);
$expected = json_encode($api);
Util::merge($api, [], false);
$actual = json_encode($api);
$this->assertSame($expected, $actual);
Util::merge($api, [], true);
$actual = json_encode($api);
$this->assertSame($expected, $actual);
}
/**
* @dataProvider provideMergeData
*/
public function testMerge($setup, $merge, $assert)
{
$api = new OA\OpenApi($setup);
Util::merge($api, $merge, false);
$this->assertTrue($api->validate());
$actual = json_decode(json_encode($api), true);
$this->assertEquals($assert, $actual);
}
public function provideMergeData(): array
{
$no = 'do not overwrite';
$yes = 'do overwrite';
$requiredInfo = ['title' => '', 'version' => ''];
$setupDefaults = [
'info' => new OA\Info($requiredInfo),
'paths' => [],
];
$assertDefaults = [
'info' => $requiredInfo,
'openapi' => '3.0.0',
'paths' => [],
];
return [[
// simple child merge
'setup' => [
'info' => new OA\Info(['version' => $no]),
'paths' => [],
],
'merge' => [
'info' => ['title' => $yes, 'version' => $yes],
],
'assert' => [
'info' => ['title' => $yes, 'version' => $no],
] + $assertDefaults,
], [
// indexed collection merge
'setup' => [
'components' => new OA\Components([
'schemas' => [
new OA\Schema(['schema' => $no, 'title' => $no]),
],
]),
] + $setupDefaults,
'merge' => [
'components' => [
'schemas' => [
$no => ['title' => $yes, 'description' => $yes],
],
],
],
'assert' => [
'components' => [
'schemas' => [
$no => ['title' => $no, 'description' => $yes],
],
],
] + $assertDefaults,
], [
// collection merge
'setup' => [
'tags' => [new OA\Tag(['name' => $no])],
] + $setupDefaults,
'merge' => [
'tags' => [
// this is actually appending right now, no clue if this is wanted,
// but the complete NelmioApiDocBundle test suite is not upset by this fact
['name' => $yes],
// this should not append since a tag with exactly the same properties
// is already present
['name' => $no],
// this does, but should not append since the name already exists, and the
// docs in Tag state that the tag names must be unique, but it is complicated
// and $api->validate() does not complain either
['name' => $no, 'description' => $yes],
],
],
'assert' => [
'tags' => [
['name' => $no],
['name' => $yes],
['name' => $no, 'description' => $yes],
],
] + $assertDefaults,
],
[
// heavy nested merge array
'setup' => $setupDefaults,
'merge' => $merge = [
'servers' => [
['url' => 'http'],
['url' => 'https'],
],
'paths' => [
'/path/to/resource' => [
'get' => [
'responses' => [
'200' => [
'$ref' => '#/components/responses/default',
],
],
'requestBody' => [
'description' => 'request foo',
'content' => [
'foo-request' => [
'schema' => [
'type' => 'object',
'required' => ['baz', 'bar'],
],
],
],
],
],
],
],
'tags' => [
['name' => 'baz'],
['name' => 'foo'],
['name' => 'baz'],
['name' => 'foo'],
['name' => 'foo'],
],
'components' => [
'responses' => [
'default' => [
'description' => 'default response',
'headers' => [
'foo-header' => [
'schema' => [
'type' => 'array',
'items' => [
'type' => 'string',
'enum' => ['foo', 'bar', 'baz'],
],
],
],
],
],
],
],
],
'assert' => array_merge(
$assertDefaults,
$merge,
['tags' => \array_slice($merge['tags'], 0, 2, true)]
),
], [
// heavy nested merge array object
'setup' => $setupDefaults,
'merge' => new \ArrayObject([
'servers' => [
['url' => 'http'],
['url' => 'https'],
],
'paths' => [
'/path/to/resource' => [
'get' => new \ArrayObject([
'responses' => [
'200' => [
'$ref' => '#/components/responses/default',
],
],
'requestBody' => new \ArrayObject([
'description' => 'request foo',
'content' => [
'foo-request' => [
'schema' => [
'required' => ['baz', 'bar'],
'type' => 'object',
],
],
],
]),
]),
],
],
'tags' => new \ArrayObject([
['name' => 'baz'],
['name' => 'foo'],
new \ArrayObject(['name' => 'baz']),
['name' => 'foo'],
['name' => 'foo'],
]),
'components' => new \ArrayObject([
'responses' => [
'default' => [
'description' => 'default response',
'headers' => new \ArrayObject([
'foo-header' => new \ArrayObject([
'schema' => new \ArrayObject([
'type' => 'array',
'items' => new \ArrayObject([
'type' => 'string',
'enum' => ['foo', 'bar', 'baz'],
]),
]),
]),
]),
],
],
]),
]),
'assert' => array_merge(
$assertDefaults,
$merge,
['tags' => \array_slice($merge['tags'], 0, 2, true)]
),
], [
// heavy nested merge swagger instance
'setup' => $setupDefaults,
'merge' => new OA\OpenApi([
'servers' => [
new OA\Server(['url' => 'http']),
new OA\Server(['url' => 'https']),
],
'paths' => [
new OA\PathItem([
'path' => '/path/to/resource',
'get' => new OA\Get([
'responses' => [
new OA\Response([
'response' => '200',
'ref' => '#/components/responses/default',
]),
],
'requestBody' => new OA\RequestBody([
'description' => 'request foo',
'content' => [
new OA\MediaType([
'mediaType' => 'foo-request',
'schema' => new OA\Schema([
'type' => 'object',
'required' => ['baz', 'bar'],
]),
]),
],
]),
]),
]),
],
'tags' => [
new OA\Tag(['name' => 'baz']),
new OA\Tag(['name' => 'foo']),
new OA\Tag(['name' => 'baz']),
new OA\Tag(['name' => 'foo']),
new OA\Tag(['name' => 'foo']),
],
'components' => new OA\Components([
'responses' => [
new OA\Response([
'response' => 'default',
'description' => 'default response',
'headers' => [
new OA\Header([
'header' => 'foo-header',
'schema' => new OA\Schema([
'type' => 'array',
'items' => new OA\Items([
'type' => 'string',
'enum' => ['foo', 'bar', 'baz'],
]),
]),
]),
],
]),
],
]),
]),
'assert' => array_merge(
$assertDefaults,
$merge,
['tags' => \array_slice($merge['tags'], 0, 2, true)]
),
], ];
}
public function assertIsNested(OA\AbstractAnnotation $parent, OA\AbstractAnnotation $child)
{
self::assertTrue($child->_context->is('nested'));
self::assertSame($parent, $child->_context->nested);
}
public function assertIsConnectedToRootContext(OA\AbstractAnnotation $annotation)
{
$this->assertSame($this->rootContext, $annotation->_context->getRootContext());
}
private function getSetupPropertiesWithoutClass(array $setup)
{
return array_filter($setup, function ($k) {return 'class' !== $k; }, ARRAY_FILTER_USE_KEY);
}
private function getNonDefaultProperties($object)
{
$objectVars = \get_object_vars($object);
$classVars = \get_class_vars(\get_class($object));
$props = [];
foreach ($objectVars as $key => $value) {
if ($value !== $classVars[$key] && 0 !== \strpos($key, '_')) {
$props[$key] = $value;
}
}
return $props;
}
}

28
UPGRADE-4.0.md Normal file
View File

@ -0,0 +1,28 @@
Upgrading From 3.x To 4.0
=========================
Version 4 is a major change introducing OpenAPI 3.0 support, the rebranded swagger specification, which brings a set of new interesting features. Unfortunately this required a rework to a large part of the bundle, and introduces BC breaks.
The Visual guide to "[What's new in 3.0 spec](https://blog.readme.com/an-example-filled-guide-to-swagger-3-2/)" gives more information on OpenApi 3.0.
Version 4 does not support older versions of the specification. If you need to output swagger v2 documentation, you will need to use the latest 3.x release.
The Upgrade to Swagger 3.0
--------------------------
The biggest part of the upgrade will most likely be the upgrade of the library `zircote/swagger-php` to `3.0` which introduces new annotations in order to support OpenAPI 3.0 but also changes
their namespace from ``Swagger`` to ``OpenApi``.
They created a dedicated page to help you upgrade : https://zircote.github.io/swagger-php/Migrating-to-v3.html.
Here are some additional advices that are more likely to apply to NelmioApiDocBundle users:
- Upgrade all your ``use Swagger\Annotations as SWG`` statements to ``use OpenApi\Annotations as OA;`` (to simplify the upgrade you may also stick to the ``SWG`` aliasing).
In case you changed ``SWG`` to ``OA``, upgrade all your annotations from ``@SWG\...`` to ``@OA\...``.
- Update your config in case you used inlined swagger docummentation (the field ``nelmio_api_doc.documentation``). [A tool](https://openapi-converter.herokuapp.com/) is available to help you convert it.
- In case you used ``@OA\Response(..., @OA\Schema(...))``, you should explicit your media type by using the annotation ``@OA\JsonContent`` or ``@OA\XmlContent`` instead of ``@OA\Schema``:
``@OA\Response(..., @OA\JsonContent(...))`` or ``@OA\Response(..., @OA\XmlContent(...))``.
When you use ``@Model`` directly (``@OA\Response(..., @Model(...)))``), the media type is set by default to ``json``.

View File

@ -16,33 +16,33 @@
], ],
"require": { "require": {
"php": "^7.1", "php": "^7.1",
"symfony/framework-bundle": "^3.4|^4.0|^5.0", "ext-json": "*",
"symfony/options-resolver": "^3.4.4|^4.0|^5.0", "symfony/framework-bundle": "^4.0|^5.0",
"symfony/property-info": "^3.4|^4.0|^5.0", "symfony/options-resolver": "^4.0|^5.0",
"exsyst/swagger": "^0.4.1", "symfony/property-info": "^4.0|^5.0",
"zircote/swagger-php": "^2.0.9", "zircote/swagger-php": "^3.0",
"phpdocumentor/reflection-docblock": "^3.1|^4.0|^5.0" "phpdocumentor/reflection-docblock": "^3.1|^4.0|^5.0"
}, },
"require-dev": { "require-dev": {
"symfony/templating": "^3.4|^4.0|^5.0", "symfony/templating": "^4.0|^5.0",
"symfony/twig-bundle": "^3.4|^4.0|^5.0", "symfony/twig-bundle": "^4.0|^5.0",
"symfony/asset": "^3.4|^4.0|^5.0", "symfony/asset": "^4.0|^5.0",
"symfony/console": "^3.4|^4.0|^5.0", "symfony/console": "^4.0|^5.0",
"symfony/config": "^3.4|^4.0|^5.0", "symfony/config": "^4.0|^5.0",
"symfony/validator": "^3.4|^4.0|^5.0", "symfony/validator": "^4.0|^5.0",
"symfony/property-access": "^3.4|^4.0|^5.0", "symfony/property-access": "^4.0|^5.0",
"symfony/form": "^3.4|^4.0|^5.0", "symfony/form": "^4.0|^5.0",
"symfony/dom-crawler": "^3.4|^4.0|^5.0", "symfony/dom-crawler": "^4.0|^5.0",
"symfony/browser-kit": "^3.4|^4.0|^5.0", "symfony/browser-kit": "^4.0|^5.0",
"symfony/cache": "^3.4|^4.0|^5.0", "symfony/cache": "^4.0|^5.0",
"symfony/phpunit-bridge": "^3.4.24|^4.0|^5.0", "symfony/phpunit-bridge": "^4.0|^5.0",
"symfony/stopwatch": "^3.4|^4.0|^5.0", "symfony/stopwatch": "^4.0|^5.0",
"symfony/routing": "^3.4|^4.0|^5.0", "symfony/routing": "^4.0|^5.0",
"sensio/framework-extra-bundle": "^3.0.13|^4.0|^5.0", "sensio/framework-extra-bundle": "^4.0|^5.0",
"doctrine/annotations": "^1.2", "doctrine/annotations": "^1.2",
"doctrine/common": "^2.4", "doctrine/common": "^2.4",
"api-platform/core": "^2.1.2", "api-platform/core": "^2.4",
"friendsofsymfony/rest-bundle": "^2.0", "friendsofsymfony/rest-bundle": "^2.0",
"willdurand/hateoas-bundle": "^1.0|^2.0", "willdurand/hateoas-bundle": "^1.0|^2.0",
"jms/serializer-bundle": "^2.3|^3.0", "jms/serializer-bundle": "^2.3|^3.0",