Merge branch 'master' into constraint_groups

This commit is contained in:
Guilhem Niot 2021-12-19 11:38:47 +01:00 committed by GitHub
commit ade4b6c17c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 311 additions and 144 deletions

View File

@ -24,18 +24,16 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- php-version: 7.1
composer-flags: "--prefer-lowest"
- php-version: 7.2 - php-version: 7.2
symfony-require: "^4.0" composer-flags: "--prefer-lowest"
- php-version: 7.3 - php-version: 7.3
symfony-require: "^5.0" symfony-require: "4.4.*"
- php-version: 7.4 - php-version: 7.4
symfony-require: "^4.0" symfony-require: "5.3.*"
- php-version: 7.3
symfony-require: "^5.0"
- php-version: 8.0 - php-version: 8.0
composer-flags: "--ignore-platform-reqs" symfony-require: "5.4.*"
- php-version: 8.1
symfony-require: "5.4.*"
steps: steps:
- name: "Checkout" - name: "Checkout"

View File

@ -20,6 +20,7 @@ use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Analysis; use OpenApi\Analysis;
use OpenApi\Annotations\OpenApi; use OpenApi\Annotations\OpenApi;
use OpenApi\Generator;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerAwareTrait;
@ -110,7 +111,7 @@ final class ApiDocGenerator
$defaultOperationIdProcessor = new DefaultOperationId(); $defaultOperationIdProcessor = new DefaultOperationId();
$defaultOperationIdProcessor($analysis); $defaultOperationIdProcessor($analysis);
$analysis->process(); $analysis->process((new Generator())->getProcessors());
$analysis->validate(); $analysis->validate();
if (isset($item)) { if (isset($item)) {

View File

@ -16,7 +16,7 @@ use Symfony\Component\Config\Definition\ConfigurationInterface;
final class Configuration implements ConfigurationInterface final class Configuration implements ConfigurationInterface
{ {
public function getConfigTreeBuilder() public function getConfigTreeBuilder(): TreeBuilder
{ {
$treeBuilder = new TreeBuilder('nelmio_api_doc'); $treeBuilder = new TreeBuilder('nelmio_api_doc');

View File

@ -153,14 +153,15 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
->setArgument(1, $config['media_types']); ->setArgument(1, $config['media_types']);
} }
// ApiPlatform support
$bundles = $container->getParameter('kernel.bundles'); $bundles = $container->getParameter('kernel.bundles');
if (!isset($bundles['TwigBundle'])) { if (!isset($bundles['TwigBundle']) || !class_exists('Symfony\Component\Asset\Packages')) {
$container->removeDefinition('nelmio_api_doc.controller.swagger_ui'); $container->removeDefinition('nelmio_api_doc.controller.swagger_ui');
$container->removeDefinition('nelmio_api_doc.render_docs.html'); $container->removeDefinition('nelmio_api_doc.render_docs.html');
$container->removeDefinition('nelmio_api_doc.render_docs.html.asset'); $container->removeDefinition('nelmio_api_doc.render_docs.html.asset');
} }
// ApiPlatform support
if (isset($bundles['ApiPlatformBundle']) && class_exists('ApiPlatform\Core\Documentation\Documentation')) { if (isset($bundles['ApiPlatformBundle']) && class_exists('ApiPlatform\Core\Documentation\Documentation')) {
$loader->load('api_platform.xml'); $loader->load('api_platform.xml');
} }

View File

@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\Describer;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
/** /**
* Makes the swagger documentation valid even if there are missing fields. * Makes the swagger documentation valid even if there are missing fields.
@ -26,22 +27,22 @@ final class DefaultDescriber implements DescriberInterface
// Info // Info
/** @var OA\Info $info */ /** @var OA\Info $info */
$info = Util::getChild($api, OA\Info::class); $info = Util::getChild($api, OA\Info::class);
if (OA\UNDEFINED === $info->title) { if (Generator::UNDEFINED === $info->title) {
$info->title = ''; $info->title = '';
} }
if (OA\UNDEFINED === $info->version) { if (Generator::UNDEFINED === $info->version) {
$info->version = '0.0.0'; $info->version = '0.0.0';
} }
// Paths // Paths
if (OA\UNDEFINED === $api->paths) { if (Generator::UNDEFINED === $api->paths) {
$api->paths = []; $api->paths = [];
} }
foreach ($api->paths as $path) { foreach ($api->paths as $path) {
foreach (Util::OPERATIONS as $method) { foreach (Util::OPERATIONS as $method) {
/** @var OA\Operation $operation */ /** @var OA\Operation $operation */
$operation = $path->{$method}; $operation = $path->{$method};
if (OA\UNDEFINED !== $operation && null !== $operation && (OA\UNDEFINED === $operation->responses || empty($operation->responses))) { if (Generator::UNDEFINED !== $operation && null !== $operation && (Generator::UNDEFINED === $operation->responses || empty($operation->responses))) {
/** @var OA\Response $response */ /** @var OA\Response $response */
$response = Util::getIndexedCollectionItem($operation, OA\Response::class, 'default'); $response = Util::getIndexedCollectionItem($operation, OA\Response::class, 'default');
$response->description = ''; $response->description = '';

View File

@ -16,8 +16,9 @@ use Nelmio\ApiDocBundle\Annotation\Operation;
use Nelmio\ApiDocBundle\Annotation\Security; use Nelmio\ApiDocBundle\Annotation\Security;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\Util\ControllerReflector; use Nelmio\ApiDocBundle\Util\ControllerReflector;
use OpenApi\Analyser; use Nelmio\ApiDocBundle\Util\SetsContextTrait;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\RouteCollection;
@ -27,6 +28,8 @@ class_exists(OA\OpenApi::class);
final class OpenApiPhpDescriber final class OpenApiPhpDescriber
{ {
use SetsContextTrait;
private $routeCollection; private $routeCollection;
private $controllerReflector; private $controllerReflector;
private $annotationReader; private $annotationReader;
@ -47,16 +50,18 @@ final class OpenApiPhpDescriber
$classAnnotations = []; $classAnnotations = [];
/** @var \ReflectionMethod $method */ /** @var \ReflectionMethod $method */
foreach ($this->getMethodsToParse() as $method => list($path, $httpMethods)) { foreach ($this->getMethodsToParse() as $method => list($path, $httpMethods, $routeName)) {
$declaringClass = $method->getDeclaringClass(); $declaringClass = $method->getDeclaringClass();
$path = Util::getPath($api, $path); $path = Util::getPath($api, $path);
Analyser::$context = Util::createContext(['nested' => $path], $path->_context); $context = Util::createContext(['nested' => $path], $path->_context);
Analyser::$context->namespace = $method->getNamespaceName(); $context->namespace = $method->getNamespaceName();
Analyser::$context->class = $declaringClass->getShortName(); $context->class = $declaringClass->getShortName();
Analyser::$context->method = $method->name; $context->method = $method->name;
Analyser::$context->filename = $method->getFileName(); $context->filename = $method->getFileName();
$this->setContext($context);
if (!array_key_exists($declaringClass->getName(), $classAnnotations)) { if (!array_key_exists($declaringClass->getName(), $classAnnotations)) {
$classAnnotations = array_filter($this->annotationReader->getClassAnnotations($declaringClass), function ($v) { $classAnnotations = array_filter($this->annotationReader->getClassAnnotations($declaringClass), function ($v) {
@ -90,7 +95,7 @@ final class OpenApiPhpDescriber
if (!in_array($annotation->method, $httpMethods, true)) { if (!in_array($annotation->method, $httpMethods, true)) {
continue; continue;
} }
if (OA\UNDEFINED !== $annotation->path && $path->path !== $annotation->path) { if (Generator::UNDEFINED !== $annotation->path && $path->path !== $annotation->path) {
continue; continue;
} }
@ -134,16 +139,20 @@ final class OpenApiPhpDescriber
$operation = Util::getOperation($path, $httpMethod); $operation = Util::getOperation($path, $httpMethod);
$operation->merge($implicitAnnotations); $operation->merge($implicitAnnotations);
$operation->mergeProperties($mergeProperties); $operation->mergeProperties($mergeProperties);
if (Generator::UNDEFINED === $operation->operationId) {
$operation->operationId = $httpMethod.'_'.$routeName;
}
} }
} }
// Reset the Analyser after the parsing // Reset the Generator after the parsing
Analyser::$context = null; $this->setContext(null);
} }
private function getMethodsToParse(): \Generator private function getMethodsToParse(): \Generator
{ {
foreach ($this->routeCollection->all() as $route) { foreach ($this->routeCollection->all() as $routeName => $route) {
if (!$route->hasDefault('_controller')) { if (!$route->hasDefault('_controller')) {
continue; continue;
} }
@ -161,7 +170,7 @@ final class OpenApiPhpDescriber
continue; continue;
} }
yield $reflectedMethod => [$path, $supportedHttpMethods]; yield $reflectedMethod => [$path, $supportedHttpMethods, $routeName];
} }
} }

View File

@ -144,8 +144,8 @@ final class ModelRegistry
'built_in_type' => $type->getBuiltinType(), 'built_in_type' => $type->getBuiltinType(),
'nullable' => $type->isNullable(), 'nullable' => $type->isNullable(),
'collection' => $type->isCollection(), 'collection' => $type->isCollection(),
'collection_key_types' => $type->isCollection() ? array_map($getType, $type->getCollectionKeyTypes()) : null, 'collection_key_types' => $type->isCollection() ? array_map($getType, $this->getCollectionKeyTypes($type)) : null,
'collection_value_types' => $type->isCollection() ? array_map($getType, $type->getCollectionValueTypes()) : null, 'collection_value_types' => $type->isCollection() ? array_map($getType, $this->getCollectionValueTypes($type)) : null,
]; ];
}; };
@ -186,6 +186,26 @@ final class ModelRegistry
} }
} }
private function getCollectionKeyTypes(Type $type): array
{
// BC layer, this condition should be removed after removing support for symfony < 5.3
if (!method_exists($type, 'getCollectionKeyTypes')) {
return null !== $type->getCollectionKeyType() ? [$type->getCollectionKeyType()] : [];
}
return $type->getCollectionKeyTypes();
}
private function getCollectionValueTypes(Type $type): array
{
// BC layer, this condition should be removed after removing support for symfony < 5.3
if (!method_exists($type, 'getCollectionValueTypes')) {
return null !== $type->getCollectionValueType() ? [$type->getCollectionValueType()] : [];
}
return $type->getCollectionValueTypes();
}
private function getCollectionValueType(Type $type): ?Type private function getCollectionValueType(Type $type): ?Type
{ {
// BC layer, this condition should be removed after removing support for symfony < 5.3 // BC layer, this condition should be removed after removing support for symfony < 5.3

View File

@ -15,16 +15,19 @@ use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister; use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Analyser; use Nelmio\ApiDocBundle\Util\SetsContextTrait;
use OpenApi\Analysis; use OpenApi\Analysis;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Context; use OpenApi\Context;
use OpenApi\Generator;
/** /**
* @internal * @internal
*/ */
class OpenApiAnnotationsReader class OpenApiAnnotationsReader
{ {
use SetsContextTrait;
private $annotationsReader; private $annotationsReader;
private $modelRegister; private $modelRegister;
@ -60,19 +63,20 @@ class OpenApiAnnotationsReader
return $default; return $default;
} }
return OA\UNDEFINED !== $oaProperty->property ? $oaProperty->property : $default; return Generator::UNDEFINED !== $oaProperty->property ? $oaProperty->property : $default;
} }
public function updateProperty($reflection, OA\Property $property, array $serializationGroups = null): void public function updateProperty($reflection, OA\Property $property, array $serializationGroups = null): void
{ {
// In order to have nicer errors // In order to have nicer errors
$declaringClass = $reflection->getDeclaringClass(); $declaringClass = $reflection->getDeclaringClass();
Analyser::$context = new Context([
$this->setContext(new Context([
'namespace' => $declaringClass->getNamespaceName(), 'namespace' => $declaringClass->getNamespaceName(),
'class' => $declaringClass->getShortName(), 'class' => $declaringClass->getShortName(),
'property' => $reflection->name, 'property' => $reflection->name,
'filename' => $declaringClass->getFileName(), 'filename' => $declaringClass->getFileName(),
]); ]));
/** @var OA\Property $oaProperty */ /** @var OA\Property $oaProperty */
if ($reflection instanceof \ReflectionProperty && !$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflection, OA\Property::class)) { if ($reflection instanceof \ReflectionProperty && !$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflection, OA\Property::class)) {
@ -80,7 +84,7 @@ class OpenApiAnnotationsReader
} elseif ($reflection instanceof \ReflectionMethod && !$oaProperty = $this->annotationsReader->getMethodAnnotation($reflection, OA\Property::class)) { } elseif ($reflection instanceof \ReflectionMethod && !$oaProperty = $this->annotationsReader->getMethodAnnotation($reflection, OA\Property::class)) {
return; return;
} }
Analyser::$context = null; $this->setContext(null);
// Read @Model annotations // Read @Model annotations
$this->modelRegister->__invoke(new Analysis([$oaProperty], Util::createContext()), $serializationGroups); $this->modelRegister->__invoke(new Analysis([$oaProperty], Util::createContext()), $serializationGroups);

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations; namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
use phpDocumentor\Reflection\DocBlock\Tags\Var_; use phpDocumentor\Reflection\DocBlock\Tags\Var_;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
@ -53,10 +54,10 @@ class PropertyPhpDocReader
} }
} }
} }
if (OA\UNDEFINED === $property->title && $title) { if (Generator::UNDEFINED === $property->title && $title) {
$property->title = $title; $property->title = $title;
} }
if (OA\UNDEFINED === $property->description && $docBlock->getDescription() && $docBlock->getDescription()->render()) { if (Generator::UNDEFINED === $property->description && $docBlock->getDescription() && $docBlock->getDescription()->render()) {
$property->description = $docBlock->getDescription()->render(); $property->description = $docBlock->getDescription()->render();
} }
} }

View File

@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@ -79,7 +80,7 @@ class SymfonyConstraintAnnotationReader
return; return;
} }
$existingRequiredFields = OA\UNDEFINED !== $this->schema->required ? $this->schema->required : []; $existingRequiredFields = Generator::UNDEFINED !== $this->schema->required ? $this->schema->required : [];
$existingRequiredFields[] = $propertyName; $existingRequiredFields[] = $propertyName;
$this->schema->required = array_values(array_unique($existingRequiredFields)); $this->schema->required = array_values(array_unique($existingRequiredFields));
@ -137,7 +138,7 @@ class SymfonyConstraintAnnotationReader
} }
foreach ($this->schema->properties as $schemaProperty) { foreach ($this->schema->properties as $schemaProperty) {
if ($schemaProperty === $property) { if ($schemaProperty === $property) {
return OA\UNDEFINED !== $schemaProperty->property ? $schemaProperty->property : null; return Generator::UNDEFINED !== $schemaProperty->property ? $schemaProperty->property : null;
} }
} }
@ -152,7 +153,7 @@ class SymfonyConstraintAnnotationReader
if (null === $newPattern) { if (null === $newPattern) {
return; return;
} }
if (OA\UNDEFINED !== $property->pattern) { if (Generator::UNDEFINED !== $property->pattern) {
$property->pattern = sprintf('%s, %s', $property->pattern, $newPattern); $property->pattern = sprintf('%s, %s', $property->pattern, $newPattern);
} else { } else {
$property->pattern = $newPattern; $property->pattern = $newPattern;

View File

@ -18,6 +18,7 @@ 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\OpenApiPhp\Util;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
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\FormConfigInterface; use Symfony\Component\Form\FormConfigInterface;
@ -101,7 +102,7 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
$property = Util::getProperty($schema, $name); $property = Util::getProperty($schema, $name);
if ($config->getRequired()) { if ($config->getRequired()) {
$required = OA\UNDEFINED !== $schema->required ? $schema->required : []; $required = Generator::UNDEFINED !== $schema->required ? $schema->required : [];
$required[] = $name; $required[] = $name;
$schema->required = $required; $schema->required = $required;
@ -111,7 +112,7 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
$property->mergeProperties($config->getOption('documentation')); $property->mergeProperties($config->getOption('documentation'));
} }
if (OA\UNDEFINED !== $property->type) { if (Generator::UNDEFINED !== $property->type) {
continue; // Type manually defined continue; // Type manually defined
} }

View File

@ -23,6 +23,7 @@ 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\OpenApiPhp\Util;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
/** /**
@ -146,7 +147,7 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
$annotationsReader->updateProperty($reflection, $property, $groups); $annotationsReader->updateProperty($reflection, $property, $groups);
} }
if (OA\UNDEFINED !== $property->type || OA\UNDEFINED !== $property->ref) { if (Generator::UNDEFINED !== $property->type || Generator::UNDEFINED !== $property->ref) {
$context->popPropertyMetadata(); $context->popPropertyMetadata();
continue; continue;

View File

@ -20,6 +20,7 @@ use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface; use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
@ -81,7 +82,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
$annotationsReader->updateDefinition($reflClass, $schema); $annotationsReader->updateDefinition($reflClass, $schema);
$discriminatorMap = $this->doctrineReader->getClassAnnotation($reflClass, DiscriminatorMap::class); $discriminatorMap = $this->doctrineReader->getClassAnnotation($reflClass, DiscriminatorMap::class);
if ($discriminatorMap && OA\UNDEFINED === $schema->discriminator) { if ($discriminatorMap && Generator::UNDEFINED === $schema->discriminator) {
$this->applyOpenApiDiscriminator( $this->applyOpenApiDiscriminator(
$model, $model,
$schema, $schema,
@ -123,7 +124,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
} }
// If type manually defined // If type manually defined
if (OA\UNDEFINED !== $property->type || OA\UNDEFINED !== $property->ref) { if (Generator::UNDEFINED !== $property->type || Generator::UNDEFINED !== $property->ref) {
continue; continue;
} }

View File

@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\OpenApiPhp;
use OpenApi\Analysis; use OpenApi\Analysis;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
/** /**
* Disable the OperationId processor from zircote/swagger-php as it breaks our documentation by setting non-unique operation ids. * Disable the OperationId processor from zircote/swagger-php as it breaks our documentation by setting non-unique operation ids.
@ -27,7 +28,7 @@ final class DefaultOperationId
$allOperations = $analysis->getAnnotationsOfType(OA\Operation::class); $allOperations = $analysis->getAnnotationsOfType(OA\Operation::class);
foreach ($allOperations as $operation) { foreach ($allOperations as $operation) {
if (OA\UNDEFINED === $operation->operationId) { if (Generator::UNDEFINED === $operation->operationId) {
$operation->operationId = null; $operation->operationId = null;
} }
} }

View File

@ -13,7 +13,7 @@ namespace Nelmio\ApiDocBundle\OpenApiPhp;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Context; use OpenApi\Context;
use const OpenApi\UNDEFINED; use OpenApi\Generator;
/** /**
* Class Util. * Class Util.
@ -163,7 +163,7 @@ final class Util
$nested = $parent::$_nested; $nested = $parent::$_nested;
$property = $nested[$class]; $property = $nested[$class];
if (null === $parent->{$property} || UNDEFINED === $parent->{$property}) { if (null === $parent->{$property} || Generator::UNDEFINED === $parent->{$property}) {
$parent->{$property} = self::createChild($parent, $class, $properties); $parent->{$property} = self::createChild($parent, $class, $properties);
} }
@ -192,7 +192,7 @@ final class Util
if (!empty($properties)) { if (!empty($properties)) {
$key = self::searchCollectionItem( $key = self::searchCollectionItem(
$parent->{$collection} && UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [], $parent->{$collection} && Generator::UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [],
$properties $properties
); );
} }
@ -224,7 +224,7 @@ final class Util
[$collection, $property] = $nested[$class]; [$collection, $property] = $nested[$class];
$key = self::searchIndexedCollectionItem( $key = self::searchIndexedCollectionItem(
$parent->{$collection} && UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [], $parent->{$collection} && Generator::UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [],
$property, $property,
$value $value
); );
@ -279,7 +279,7 @@ final class Util
*/ */
public static function createCollectionItem(OA\AbstractAnnotation $parent, $collection, $class, array $properties = []): int public static function createCollectionItem(OA\AbstractAnnotation $parent, $collection, $class, array $properties = []): int
{ {
if (UNDEFINED === $parent->{$collection}) { if (Generator::UNDEFINED === $parent->{$collection}) {
$parent->{$collection} = []; $parent->{$collection} = [];
} }
@ -418,7 +418,7 @@ final class Util
if (\is_string($type) && 0 === strpos($type, '[')) { if (\is_string($type) && 0 === strpos($type, '[')) {
$innerType = substr($type, 1, -1); $innerType = substr($type, 1, -1);
if (!$annotation->{$propertyName} || UNDEFINED === $annotation->{$propertyName}) { if (!$annotation->{$propertyName} || Generator::UNDEFINED === $annotation->{$propertyName}) {
$annotation->{$propertyName} = []; $annotation->{$propertyName} = [];
} }

View File

@ -15,6 +15,7 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
class CompoundPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface class CompoundPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface
{ {
@ -30,7 +31,7 @@ class CompoundPropertyDescriber implements PropertyDescriberInterface, ModelRegi
public function describe(array $types, OA\Schema $property, array $groups = null) public function describe(array $types, OA\Schema $property, array $groups = null)
{ {
$property->oneOf = OA\UNDEFINED !== $property->oneOf ? $property->oneOf : []; $property->oneOf = Generator::UNDEFINED !== $property->oneOf ? $property->oneOf : [];
foreach ($types as $type) { foreach ($types as $type) {
$property->oneOf[] = $schema = Util::createChild($property, OA\Schema::class, []); $property->oneOf[] = $schema = Util::createChild($property, OA\Schema::class, []);

View File

@ -29,10 +29,7 @@ class ObjectPropertyDescriber implements PropertyDescriberInterface, ModelRegist
$types[0]->getClassName(), $types[0]->getClassName(),
$types[0]->isCollection(), $types[0]->isCollection(),
// BC layer for symfony < 5.3 // BC layer for symfony < 5.3
method_exists($types[0], 'getCollectionKeyTypes') ? method_exists($types[0], 'getCollectionKeyTypes') ? $types[0]->getCollectionKeyTypes() : $types[0]->getCollectionKeyType(),
($types[0]->getCollectionKeyTypes()[0] ?? null) :
$types[0]->getCollectionKeyType(),
// BC layer for symfony < 5.3
method_exists($types[0], 'getCollectionValueTypes') ? method_exists($types[0], 'getCollectionValueTypes') ?
($types[0]->getCollectionValueTypes()[0] ?? null) : ($types[0]->getCollectionValueTypes()[0] ?? null) :
$types[0]->getCollectionValueType() $types[0]->getCollectionValueType()

View File

@ -31,7 +31,7 @@ class GetNelmioAsset extends AbstractExtension
$this->resourcesDir = __DIR__.'/../../Resources/public'; $this->resourcesDir = __DIR__.'/../../Resources/public';
} }
public function getFunctions() public function getFunctions(): array
{ {
return [ return [
new TwigFunction('nelmioAsset', $this, ['is_safe' => ['html']]), new TwigFunction('nelmioAsset', $this, ['is_safe' => ['html']]),

View File

@ -39,7 +39,7 @@ Open a command console, enter your project directory and execute the following c
class AppKernel extends Kernel class AppKernel extends Kernel
{ {
public function registerBundles() public function registerBundles(): iterable
{ {
$bundles = [ $bundles = [
// ... // ...

View File

@ -51,6 +51,8 @@ file that was distributed with this source code. #}
</defs> </defs>
</svg> </svg>
{% endblock svg_icons %} {% endblock svg_icons %}
{% block header_block %}
<header> <header>
{% block header %} {% block header %}
<a id="logo" href="https://github.com/nelmio/NelmioApiDocBundle"> <a id="logo" href="https://github.com/nelmio/NelmioApiDocBundle">
@ -58,6 +60,7 @@ file that was distributed with this source code. #}
</a> </a>
{% endblock header %} {% endblock header %}
</header> </header>
{% endblock header_block %}
{% block swagger_ui %} {% block swagger_ui %}
<div id="swagger-ui" class="api-platform"></div> <div id="swagger-ui" class="api-platform"></div>

View File

@ -16,6 +16,7 @@ use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\RequestParam; use FOS\RestBundle\Controller\Annotations\RequestParam;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
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\DateTime; use Symfony\Component\Validator\Constraints\DateTime;
@ -55,7 +56,7 @@ final class FosRestDescriber implements RouteDescriberInterface
$parameter->required = !$annotation->nullable && $annotation->strict; $parameter->required = !$annotation->nullable && $annotation->strict;
if (OA\UNDEFINED === $parameter->description) { if (Generator::UNDEFINED === $parameter->description) {
$parameter->description = $annotation->description; $parameter->description = $annotation->description;
} }
@ -128,7 +129,7 @@ final class FosRestDescriber implements RouteDescriberInterface
private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema
{ {
$requestBody->content = OA\UNDEFINED !== $requestBody->content ? $requestBody->content : []; $requestBody->content = Generator::UNDEFINED !== $requestBody->content ? $requestBody->content : [];
switch ($type) { switch ($type) {
case 'json': case 'json':
$contentType = 'application/json'; $contentType = 'application/json';
@ -165,7 +166,7 @@ final class FosRestDescriber implements RouteDescriberInterface
{ {
$schema->default = $annotation->getDefault(); $schema->default = $annotation->getDefault();
if (OA\UNDEFINED === $schema->type) { if (Generator::UNDEFINED === $schema->type) {
$schema->type = $annotation->map ? 'array' : 'string'; $schema->type = $annotation->map ? 'array' : 'string';
} }

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\RouteDescriber; namespace Nelmio\ApiDocBundle\RouteDescriber;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
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;
@ -47,10 +48,10 @@ 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 (OA\UNDEFINED === $operation->summary && '' !== $docBlock->getSummary()) { if (Generator::UNDEFINED === $operation->summary && '' !== $docBlock->getSummary()) {
$operation->summary = $docBlock->getSummary(); $operation->summary = $docBlock->getSummary();
} }
if (OA\UNDEFINED === $operation->description && '' !== (string) $docBlock->getDescription()) { if (Generator::UNDEFINED === $operation->description && '' !== (string) $docBlock->getDescription()) {
$operation->description = (string) $docBlock->getDescription(); $operation->description = (string) $docBlock->getDescription();
} }
if ($docBlock->hasTag('deprecated')) { if ($docBlock->hasTag('deprecated')) {

View File

@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\RouteDescriber;
use LogicException; use LogicException;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
/** /**
@ -40,7 +41,7 @@ final class RouteMetadataDescriber implements RouteDescriberInterface
/** @var OA\Parameter $parameter */ /** @var OA\Parameter $parameter */
$parameter = $existingParams[$paramId] ?? null; $parameter = $existingParams[$paramId] ?? null;
if (null !== $parameter) { if (null !== $parameter) {
if (!$parameter->required || OA\UNDEFINED === $parameter->required) { if (!$parameter->required || Generator::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()));
} }
@ -52,11 +53,11 @@ final class RouteMetadataDescriber implements RouteDescriberInterface
$parameter->schema = Util::getChild($parameter, OA\Schema::class); $parameter->schema = Util::getChild($parameter, OA\Schema::class);
if (OA\UNDEFINED === $parameter->schema->type) { if (Generator::UNDEFINED === $parameter->schema->type) {
$parameter->schema->type = 'string'; $parameter->schema->type = 'string';
} }
if (isset($requirements[$pathVariable]) && OA\UNDEFINED === $parameter->schema->pattern) { if (isset($requirements[$pathVariable]) && Generator::UNDEFINED === $parameter->schema->pattern) {
$parameter->schema->pattern = $requirements[$pathVariable]; $parameter->schema->pattern = $requirements[$pathVariable];
} }
} }
@ -71,15 +72,15 @@ final class RouteMetadataDescriber implements RouteDescriberInterface
private function getRefParams(OA\OpenApi $api, OA\Operation $operation): array private function getRefParams(OA\OpenApi $api, OA\Operation $operation): array
{ {
/** @var OA\Parameter[] $globalParams */ /** @var OA\Parameter[] $globalParams */
$globalParams = OA\UNDEFINED !== $api->components && OA\UNDEFINED !== $api->components->parameters ? $api->components->parameters : []; $globalParams = Generator::UNDEFINED !== $api->components && Generator::UNDEFINED !== $api->components->parameters ? $api->components->parameters : [];
$globalParams = array_column($globalParams, null, 'parameter'); // update the indexes of the array with the reference names actually used $globalParams = array_column($globalParams, null, 'parameter'); // update the indexes of the array with the reference names actually used
$existingParams = []; $existingParams = [];
$operationParameters = OA\UNDEFINED !== $operation->parameters ? $operation->parameters : []; $operationParameters = Generator::UNDEFINED !== $operation->parameters ? $operation->parameters : [];
/** @var OA\Parameter $parameter */ /** @var OA\Parameter $parameter */
foreach ($operationParameters as $id => $parameter) { foreach ($operationParameters as $id => $parameter) {
$ref = $parameter->ref; $ref = $parameter->ref;
if (OA\UNDEFINED === $ref) { if (Generator::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;
} }

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
use Nelmio\ApiDocBundle\Exception\UndocumentedArrayItemsException; use Nelmio\ApiDocBundle\Exception\UndocumentedArrayItemsException;
use Symfony\Component\HttpKernel\KernelInterface;
class ArrayItemsErrorTest extends WebTestCase class ArrayItemsErrorTest extends WebTestCase
{ {
@ -30,7 +31,7 @@ class ArrayItemsErrorTest extends WebTestCase
$this->getOpenApiDefinition(); $this->getOpenApiDefinition();
} }
protected static function createKernel(array $options = []) protected static function createKernel(array $options = []): KernelInterface
{ {
return new TestKernel(TestKernel::ERROR_ARRAY_ITEMS); return new TestKernel(TestKernel::ERROR_ARRAY_ITEMS);
} }

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
use Hateoas\Configuration\Embedded; use Hateoas\Configuration\Embedded;
use Symfony\Component\HttpKernel\KernelInterface;
class BazingaFunctionalTest extends WebTestCase class BazingaFunctionalTest extends WebTestCase
{ {
@ -123,7 +124,7 @@ class BazingaFunctionalTest extends WebTestCase
], json_decode($this->getModel('BazingaUserTyped')->toJson(), true)); ], json_decode($this->getModel('BazingaUserTyped')->toJson(), true));
} }
protected static function createKernel(array $options = []) protected static function createKernel(array $options = []): KernelInterface
{ {
return new TestKernel(TestKernel::USE_JMS | TestKernel::USE_BAZINGA); return new TestKernel(TestKernel::USE_JMS | TestKernel::USE_BAZINGA);
} }

View File

@ -27,7 +27,7 @@ use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
/** /**
* @Route("/api", host="api.example.com") * @Route("/api", name="api_", host="api.example.com")
*/ */
class ApiController class ApiController
{ {
@ -245,4 +245,23 @@ class ApiController
public function discriminatorMappingAction() public function discriminatorMappingAction()
{ {
} }
/**
* @Route("/named_route-operation-id", name="named_route_operation_id", methods={"GET", "POST"})
*
* @OA\Response(response=200, description="success")
*/
public function namedRouteOperationIdAction()
{
}
/**
* @Route("/custom-operation-id", methods={"GET", "POST"})
*
* @Operation(operationId="custom-operation-id")
* @OA\Response(response=200, description="success")
*/
public function customOperationIdAction()
{
}
} }

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
class FOSRestTest extends WebTestCase class FOSRestTest extends WebTestCase
{ {
@ -37,17 +38,17 @@ class FOSRestTest extends WebTestCase
$fooParameter = $this->getParameter($operation, 'foo', 'query'); $fooParameter = $this->getParameter($operation, 'foo', 'query');
$this->assertInstanceOf(OA\Schema::class, $fooParameter->schema); $this->assertInstanceOf(OA\Schema::class, $fooParameter->schema);
$this->assertEquals('\d+', $fooParameter->schema->pattern); $this->assertEquals('\d+', $fooParameter->schema->pattern);
$this->assertEquals(OA\UNDEFINED, $fooParameter->schema->format); $this->assertEquals(Generator::UNDEFINED, $fooParameter->schema->format);
$mappedParameter = $this->getParameter($operation, 'mapped[]', 'query'); $mappedParameter = $this->getParameter($operation, 'mapped[]', 'query');
$this->assertTrue($mappedParameter->explode); $this->assertTrue($mappedParameter->explode);
$barProperty = $this->getProperty($bodySchema, 'bar'); $barProperty = $this->getProperty($bodySchema, 'bar');
$this->assertEquals('\d+', $barProperty->pattern); $this->assertEquals('\d+', $barProperty->pattern);
$this->assertEquals(OA\UNDEFINED, $barProperty->format); $this->assertEquals(Generator::UNDEFINED, $barProperty->format);
$bazProperty = $this->getProperty($bodySchema, 'baz'); $bazProperty = $this->getProperty($bodySchema, 'baz');
$this->assertEquals(OA\UNDEFINED, $bazProperty->pattern); $this->assertEquals(Generator::UNDEFINED, $bazProperty->pattern);
$this->assertEquals('IsTrue', $bazProperty->format); $this->assertEquals('IsTrue', $bazProperty->format);
$dateTimeProperty = $this->getProperty($bodySchema, 'datetime'); $dateTimeProperty = $this->getProperty($bodySchema, 'datetime');
@ -57,7 +58,7 @@ class FOSRestTest extends WebTestCase
$this->assertEquals('date-time', $dateTimeAltProperty->format); $this->assertEquals('date-time', $dateTimeAltProperty->format);
$dateTimeNoFormatProperty = $this->getProperty($bodySchema, 'datetimeNoFormat'); $dateTimeNoFormatProperty = $this->getProperty($bodySchema, 'datetimeNoFormat');
$this->assertEquals(OA\UNDEFINED, $dateTimeNoFormatProperty->format); $this->assertEquals(Generator::UNDEFINED, $dateTimeNoFormatProperty->format);
$dateProperty = $this->getProperty($bodySchema, 'date'); $dateProperty = $this->getProperty($bodySchema, 'date');
$this->assertEquals('date', $dateProperty->format); $this->assertEquals('date', $dateProperty->format);

View File

@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\Tests\Helper; use Nelmio\ApiDocBundle\Tests\Helper;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
use Symfony\Component\Serializer\Annotation\SerializedName; use Symfony\Component\Serializer\Annotation\SerializedName;
class FunctionalTest extends WebTestCase class FunctionalTest extends WebTestCase
@ -84,7 +85,7 @@ class FunctionalTest extends WebTestCase
public function testAnnotationWithManualPath() public function testAnnotationWithManualPath()
{ {
$path = $this->getPath('/api/swagger2'); $path = $this->getPath('/api/swagger2');
$this->assertSame(OA\UNDEFINED, $path->post); $this->assertSame(Generator::UNDEFINED, $path->post);
$operation = $this->getOperation('/api/swagger', 'get'); $operation = $this->getOperation('/api/swagger', 'get');
$this->assertNotHasParameter('Accept-Version', 'header', $operation); $this->assertNotHasParameter('Accept-Version', 'header', $operation);
@ -123,10 +124,10 @@ class FunctionalTest extends WebTestCase
{ {
$operation = $this->getOperation('/api/test/{user}', 'get'); $operation = $this->getOperation('/api/test/{user}', 'get');
$this->assertEquals(OA\UNDEFINED, $operation->security); $this->assertEquals(Generator::UNDEFINED, $operation->security);
$this->assertEquals(OA\UNDEFINED, $operation->summary); $this->assertEquals(Generator::UNDEFINED, $operation->summary);
$this->assertEquals(OA\UNDEFINED, $operation->description); $this->assertEquals(Generator::UNDEFINED, $operation->description);
$this->assertEquals(OA\UNDEFINED, $operation->deprecated); $this->assertEquals(Generator::UNDEFINED, $operation->deprecated);
$this->assertHasResponse(200, $operation); $this->assertHasResponse(200, $operation);
$this->assertHasParameter('user', 'path', $operation); $this->assertHasParameter('user', 'path', $operation);
@ -134,7 +135,7 @@ class FunctionalTest extends WebTestCase
$this->assertTrue($parameter->required); $this->assertTrue($parameter->required);
$this->assertEquals('string', $parameter->schema->type); $this->assertEquals('string', $parameter->schema->type);
$this->assertEquals('/foo/', $parameter->schema->pattern); $this->assertEquals('/foo/', $parameter->schema->pattern);
$this->assertEquals(OA\UNDEFINED, $parameter->schema->format); $this->assertEquals(Generator::UNDEFINED, $parameter->schema->format);
} }
public function testDeprecatedAction() public function testDeprecatedAction()
@ -506,7 +507,25 @@ class FunctionalTest extends WebTestCase
public function testDefaultOperationId() public function testDefaultOperationId()
{ {
$operation = $this->getOperation('/api/article/{id}', 'get'); $operation = $this->getOperation('/api/article/{id}', 'get');
$this->assertNull($operation->operationId); $this->assertEquals('get_api_nelmio_apidoc_tests_functional_api_fetcharticle', $operation->operationId);
}
public function testNamedRouteOperationId()
{
$operation = $this->getOperation('/api/named_route-operation-id', 'get');
$this->assertEquals('get_api_named_route_operation_id', $operation->operationId);
$operation = $this->getOperation('/api/named_route-operation-id', 'post');
$this->assertEquals('post_api_named_route_operation_id', $operation->operationId);
}
public function testCustomOperationId()
{
$operation = $this->getOperation('/api/custom-operation-id', 'get');
$this->assertEquals('custom-operation-id', $operation->operationId);
$operation = $this->getOperation('/api/custom-operation-id', 'post');
$this->assertEquals('custom-operation-id', $operation->operationId);
} }
/** /**
@ -533,7 +552,7 @@ class FunctionalTest extends WebTestCase
$this->assertCount(2, $model->discriminator->mapping); $this->assertCount(2, $model->discriminator->mapping);
$this->assertArrayHasKey('one', $model->discriminator->mapping); $this->assertArrayHasKey('one', $model->discriminator->mapping);
$this->assertArrayHasKey('two', $model->discriminator->mapping); $this->assertArrayHasKey('two', $model->discriminator->mapping);
$this->assertNotSame(OA\UNDEFINED, $model->oneOf); $this->assertNotSame(Generator::UNDEFINED, $model->oneOf);
$this->assertCount(2, $model->oneOf); $this->assertCount(2, $model->oneOf);
} }

View File

@ -11,6 +11,8 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
use Symfony\Component\HttpKernel\KernelInterface;
class JMSFunctionalTest extends WebTestCase class JMSFunctionalTest extends WebTestCase
{ {
protected function setUp(): void protected function setUp(): void
@ -333,7 +335,7 @@ class JMSFunctionalTest extends WebTestCase
], json_decode($this->getModel('JMSNamingStrategyConstraints')->toJson(), true)); ], json_decode($this->getModel('JMSNamingStrategyConstraints')->toJson(), true));
} }
protected static function createKernel(array $options = []) protected static function createKernel(array $options = []): KernelInterface
{ {
return new TestKernel(TestKernel::USE_JMS); return new TestKernel(TestKernel::USE_JMS);
} }

View File

@ -55,7 +55,7 @@ class TestKernel extends Kernel
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function registerBundles() public function registerBundles(): iterable
{ {
$bundles = [ $bundles = [
new FrameworkBundle(), new FrameworkBundle(),
@ -288,7 +288,7 @@ class TestKernel extends Kernel
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getCacheDir() public function getCacheDir(): string
{ {
return parent::getCacheDir().'/'.$this->flags; return parent::getCacheDir().'/'.$this->flags;
} }
@ -296,7 +296,7 @@ class TestKernel extends Kernel
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getLogDir() public function getLogDir(): string
{ {
return parent::getLogDir().'/'.$this->flags; return parent::getLogDir().'/'.$this->flags;
} }

View File

@ -12,11 +12,13 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase;
use Symfony\Component\HttpKernel\KernelInterface;
class WebTestCase extends BaseWebTestCase class WebTestCase extends BaseWebTestCase
{ {
protected static function createKernel(array $options = []) protected static function createKernel(array $options = []): KernelInterface
{ {
return new TestKernel(); return new TestKernel();
} }
@ -93,7 +95,7 @@ class WebTestCase extends BaseWebTestCase
public function assertHasPath($path, OA\OpenApi $api) public function assertHasPath($path, OA\OpenApi $api)
{ {
$paths = array_column(OA\UNDEFINED !== $api->paths ? $api->paths : [], 'path'); $paths = array_column(Generator::UNDEFINED !== $api->paths ? $api->paths : [], 'path');
static::assertContains( static::assertContains(
$path, $path,
$paths, $paths,
@ -103,7 +105,7 @@ class WebTestCase extends BaseWebTestCase
public function assertNotHasPath($path, OA\OpenApi $api) public function assertNotHasPath($path, OA\OpenApi $api)
{ {
$paths = array_column(OA\UNDEFINED !== $api->paths ? $api->paths : [], 'path'); $paths = array_column(Generator::UNDEFINED !== $api->paths ? $api->paths : [], 'path');
static::assertNotContains( static::assertNotContains(
$path, $path,
$paths, $paths,
@ -113,7 +115,7 @@ class WebTestCase extends BaseWebTestCase
public function assertHasResponse($responseCode, OA\Operation $operation) public function assertHasResponse($responseCode, OA\Operation $operation)
{ {
$responses = array_column(OA\UNDEFINED !== $operation->responses ? $operation->responses : [], 'response'); $responses = array_column(Generator::UNDEFINED !== $operation->responses ? $operation->responses : [], 'response');
static::assertContains( static::assertContains(
$responseCode, $responseCode,
$responses, $responses,
@ -124,7 +126,7 @@ class WebTestCase extends BaseWebTestCase
public function assertHasParameter($name, $in, OA\AbstractAnnotation $annotation) public function assertHasParameter($name, $in, OA\AbstractAnnotation $annotation)
{ {
/* @var OA\Operation|OA\OpenApi $annotation */ /* @var OA\Operation|OA\OpenApi $annotation */
$parameters = array_filter(OA\UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], function (OA\Parameter $parameter) use ($name, $in) { $parameters = array_filter(Generator::UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], function (OA\Parameter $parameter) use ($name, $in) {
return $parameter->name === $name && $parameter->in === $in; return $parameter->name === $name && $parameter->in === $in;
}); });
@ -137,7 +139,7 @@ class WebTestCase extends BaseWebTestCase
public function assertNotHasParameter($name, $in, OA\AbstractAnnotation $annotation) public function assertNotHasParameter($name, $in, OA\AbstractAnnotation $annotation)
{ {
/* @var OA\Operation|OA\OpenApi $annotation */ /* @var OA\Operation|OA\OpenApi $annotation */
$parameters = array_column(OA\UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], 'name', 'in'); $parameters = array_column(Generator::UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], 'name', 'in');
static::assertNotContains( static::assertNotContains(
$name, $name,
$parameters[$in] ?? [], $parameters[$in] ?? [],
@ -148,7 +150,7 @@ class WebTestCase extends BaseWebTestCase
public function assertHasProperty($property, OA\AbstractAnnotation $annotation) public function assertHasProperty($property, OA\AbstractAnnotation $annotation)
{ {
/* @var OA\Schema|OA\Property|OA\Items $annotation */ /* @var OA\Schema|OA\Property|OA\Items $annotation */
$properties = array_column(OA\UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property'); $properties = array_column(Generator::UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property');
static::assertContains( static::assertContains(
$property, $property,
$properties, $properties,
@ -159,7 +161,7 @@ class WebTestCase extends BaseWebTestCase
public function assertNotHasProperty($property, OA\AbstractAnnotation $annotation) public function assertNotHasProperty($property, OA\AbstractAnnotation $annotation)
{ {
/* @var OA\Schema|OA\Property|OA\Items $annotation */ /* @var OA\Schema|OA\Property|OA\Items $annotation */
$properties = array_column(OA\UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property'); $properties = array_column(Generator::UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property');
static::assertNotContains( static::assertNotContains(
$property, $property,
$properties, $properties,

View File

@ -34,7 +34,10 @@ class ModelRegistryTest extends TestCase
$this->assertEquals('#/components/schemas/array', $registry->register(new Model($type, ['group1']))); $this->assertEquals('#/components/schemas/array', $registry->register(new Model($type, ['group1'])));
} }
public function testNameCollisionsAreLogged() /**
* @dataProvider provideNameCollisionsTypes
*/
public function testNameCollisionsAreLogged(Type $type, array $arrayType)
{ {
$logger = $this->createMock(LoggerInterface::class); $logger = $this->createMock(LoggerInterface::class);
$logger $logger
@ -43,26 +46,12 @@ class ModelRegistryTest extends TestCase
->with( ->with(
'Can not assign a name for the model, the name "ModelRegistryTest" has already been taken.', [ 'Can not assign a name for the model, the name "ModelRegistryTest" has already been taken.', [
'model' => [ 'model' => [
'type' => [ 'type' => $arrayType,
'class' => 'Nelmio\\ApiDocBundle\\Tests\\Model\\ModelRegistryTest',
'built_in_type' => 'object',
'nullable' => false,
'collection' => false,
'collection_key_types' => null,
'collection_value_types' => null,
],
'options' => null, 'options' => null,
'groups' => ['group2'], 'groups' => ['group2'],
], ],
'taken_by' => [ 'taken_by' => [
'type' => [ 'type' => $arrayType,
'class' => 'Nelmio\\ApiDocBundle\\Tests\\Model\\ModelRegistryTest',
'built_in_type' => 'object',
'nullable' => false,
'collection' => false,
'collection_key_types' => null,
'collection_value_types' => null,
],
'options' => null, 'options' => null,
'groups' => ['group1'], 'groups' => ['group1'],
], ],
@ -71,11 +60,46 @@ class ModelRegistryTest extends TestCase
$registry = new ModelRegistry([], new OA\OpenApi([]), []); $registry = new ModelRegistry([], new OA\OpenApi([]), []);
$registry->setLogger($logger); $registry->setLogger($logger);
$type = new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class);
$registry->register(new Model($type, ['group1'])); $registry->register(new Model($type, ['group1']));
$registry->register(new Model($type, ['group2'])); $registry->register(new Model($type, ['group2']));
} }
public function provideNameCollisionsTypes()
{
yield [
new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class),
[
'class' => 'Nelmio\\ApiDocBundle\\Tests\\Model\\ModelRegistryTest',
'built_in_type' => 'object',
'nullable' => false,
'collection' => false,
'collection_key_types' => null,
'collection_value_types' => null,
],
];
yield [
new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class, true, new Type(Type::BUILTIN_TYPE_OBJECT)),
[
'class' => 'Nelmio\\ApiDocBundle\\Tests\\Model\\ModelRegistryTest',
'built_in_type' => 'object',
'nullable' => false,
'collection' => true,
'collection_key_types' => [
[
'class' => null,
'built_in_type' => 'object',
'nullable' => false,
'collection' => false,
'collection_key_types' => null,
'collection_value_types' => null,
],
],
'collection_value_types' => [],
],
];
}
public function testNameCollisionsAreLoggedWithAlternativeNames() public function testNameCollisionsAreLoggedWithAlternativeNames()
{ {
$ref = new \ReflectionClass(self::class); $ref = new \ReflectionClass(self::class);

View File

@ -16,6 +16,7 @@ use Nelmio\ApiDocBundle\ModelDescriber\Annotations\SymfonyConstraintAnnotationRe
use Nelmio\ApiDocBundle\Tests\Helper; use Nelmio\ApiDocBundle\Tests\Helper;
use Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations\Fixture as CustomAssert; use Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations\Fixture as CustomAssert;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@ -191,7 +192,7 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->maxLength); $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->maxLength);
$this->assertSame(1, $schema->properties[0]->minLength); $this->assertSame(1, $schema->properties[0]->minLength);
} }
@ -227,7 +228,7 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->minLength); $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->minLength);
$this->assertSame(100, $schema->properties[0]->maxLength); $this->assertSame(100, $schema->properties[0]->maxLength);
} }
@ -273,11 +274,11 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$this->assertSame(5, $schema->properties[0]->maximum); $this->assertSame(5, $schema->properties[0]->maximum);
$this->assertTrue($schema->properties[0]->exclusiveMaximum); $this->assertTrue($schema->properties[0]->exclusiveMaximum);
} else { } else {
$this->assertSame(OA\UNDEFINED, $schema->required); $this->assertSame(Generator::UNDEFINED, $schema->required);
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->minimum); $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->minimum);
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->exclusiveMinimum); $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->exclusiveMinimum);
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->maximum); $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->maximum);
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->exclusiveMaximum); $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->exclusiveMaximum);
} }
} }
@ -296,7 +297,7 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->minItems); $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->minItems);
$this->assertSame(10, $schema->properties[0]->maxItems); $this->assertSame(10, $schema->properties[0]->maxItems);
} }
@ -332,7 +333,7 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->maxItems); $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->maxItems);
$this->assertSame(10, $schema->properties[0]->minItems); $this->assertSame(10, $schema->properties[0]->minItems);
} }
@ -368,7 +369,7 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->maximum); $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->maximum);
$this->assertSame(10, $schema->properties[0]->minimum); $this->assertSame(10, $schema->properties[0]->minimum);
} }
@ -404,7 +405,7 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->minimum); $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->minimum);
$this->assertSame(10, $schema->properties[0]->maximum); $this->assertSame(10, $schema->properties[0]->maximum);
} }

View File

@ -15,6 +15,7 @@ use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\ModelDescriber\ApplyOpenApiDiscriminatorTrait; use Nelmio\ApiDocBundle\ModelDescriber\ApplyOpenApiDiscriminatorTrait;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Generator;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
@ -57,7 +58,7 @@ class ApplyOpenApiDiscriminatorTraitTest extends TestCase
'two' => 'SecondType', 'two' => 'SecondType',
]); ]);
$this->assertNotSame(OA\UNDEFINED, $this->schema->oneOf); $this->assertNotSame(Generator::UNDEFINED, $this->schema->oneOf);
$this->assertCount(2, $this->schema->oneOf); $this->assertCount(2, $this->schema->oneOf);
$this->assertSame( $this->assertSame(
$this->modelRegistry->register($this->createModel('FirstType')), $this->modelRegistry->register($this->createModel('FirstType')),

View File

@ -14,7 +14,7 @@ namespace Nelmio\ApiDocBundle\Tests\SwaggerPhp;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use OpenApi\Context; use OpenApi\Context;
use const OpenApi\UNDEFINED; use OpenApi\Generator;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
@ -107,7 +107,7 @@ class UtilTest extends TestCase
return 0 !== strpos($key, '_'); return 0 !== strpos($key, '_');
}, ARRAY_FILTER_USE_KEY); }, ARRAY_FILTER_USE_KEY);
$this->assertEquals([UNDEFINED], array_unique(array_values($properties))); $this->assertEquals([Generator::UNDEFINED], array_unique(array_values($properties)));
$this->assertIsNested($this->rootAnnotation, $info); $this->assertIsNested($this->rootAnnotation, $info);
$this->assertIsConnectedToRootContext($info); $this->assertIsConnectedToRootContext($info);
@ -220,7 +220,7 @@ class UtilTest extends TestCase
foreach ($items as $assert) { foreach ($items as $assert) {
$setupCollection = empty($assert['components']) ? $setupCollection = empty($assert['components']) ?
($setup[$collection] ?? []) : ($setup[$collection] ?? []) :
(OA\UNDEFINED !== $setup['components']->{$collection} ? $setup['components']->{$collection} : []); (Generator::UNDEFINED !== $setup['components']->{$collection} ? $setup['components']->{$collection} : []);
// get the indexing correct within haystack preparation // get the indexing correct within haystack preparation
$properties = array_fill(0, \count($setupCollection), null); $properties = array_fill(0, \count($setupCollection), null);

View File

@ -0,0 +1,29 @@
<?php
namespace Nelmio\ApiDocBundle\Tests\Util;
use Nelmio\ApiDocBundle\Tests\Functional\Controller\BazingaController;
use Nelmio\ApiDocBundle\Util\ControllerReflector;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use Symfony\Component\DependencyInjection\Container;
class ControllerReflectorTest extends TestCase
{
public function testGetReflectionMethod(): void
{
$controllerReflector = new ControllerReflector(new Container());
$this->assertEquals(
ReflectionMethod::class,
get_class($controllerReflector->getReflectionMethod([BazingaController::class, 'userAction']))
);
$this->assertEquals(
ReflectionMethod::class,
get_class($controllerReflector->getReflectionMethod(BazingaController::class.'::userAction'))
);
$this->assertNull(
$controllerReflector->getReflectionMethod('UnknownController::userAction')
);
$this->assertNull($controllerReflector->getReflectionMethod(null));
}
}

View File

@ -38,16 +38,17 @@ class ControllerReflector
/** /**
* Returns the ReflectionMethod for the given controller string. * Returns the ReflectionMethod for the given controller string.
* *
* @return \ReflectionMethod|null * @return \ReflectionMethod|null
*/ */
public function getReflectionMethod($controller) public function getReflectionMethod($controller)
{ {
if (is_string($controller)) { if (is_string($controller)) {
$controller = $this->getClassAndMethod($controller); $controller = $this->getClassAndMethod($controller);
}
if (null === $controller) { if (null === $controller) {
return null; return null;
} }
}
return $this->geReflectionMethodByClassNameAndMethodName(...$controller); return $this->geReflectionMethodByClassNameAndMethodName(...$controller);
} }
@ -122,7 +123,7 @@ class ControllerReflector
if (!isset($class) || !isset($method)) { if (!isset($class) || !isset($method)) {
$this->controllers[$controller] = null; $this->controllers[$controller] = null;
return; return null;
} }
return $this->controllers[$controller] = [$class, $method]; return $this->controllers[$controller] = [$class, $method];

22
Util/SetsContextTrait.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace Nelmio\ApiDocBundle\Util;
use OpenApi\Context;
/**
* @internal
*/
trait SetsContextTrait
{
private function setContext(?Context $context): void
{
if (class_exists(\OpenApi\Analyser::class)) {
// zircote/swagger-php ^3.2
\OpenApi\Analyser::$context = $context;
} else {
/// zircote/swagger-php ^4.0
\OpenApi\Generator::$context = $context;
}
}
}

View File

@ -15,7 +15,7 @@
} }
], ],
"require": { "require": {
"php": ">=7.1.3", "php": ">=7.2",
"ext-json": "*", "ext-json": "*",
"doctrine/annotations": "^1.11", "doctrine/annotations": "^1.11",
"psr/cache": "^1.0|^2.0|^3.0", "psr/cache": "^1.0|^2.0|^3.0",
@ -30,7 +30,7 @@
"symfony/options-resolver": "^4.4|^5.0", "symfony/options-resolver": "^4.4|^5.0",
"symfony/property-info": "^4.4|^5.0", "symfony/property-info": "^4.4|^5.0",
"symfony/routing": "^4.4|^5.0", "symfony/routing": "^4.4|^5.0",
"zircote/swagger-php": "^3.0", "zircote/swagger-php": "^3.2|^4.0",
"phpdocumentor/reflection-docblock": "^3.1|^4.4|^5.0" "phpdocumentor/reflection-docblock": "^3.1|^4.4|^5.0"
}, },
"require-dev": { "require-dev": {