diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yaml b/.github/ISSUE_TEMPLATE/1_Bug_report.yaml index 1d8ea9513..dfd02e7f9 100644 --- a/.github/ISSUE_TEMPLATE/1_Bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yaml @@ -19,6 +19,23 @@ body: description: A clear and concise description of the problem validations: required: true + - type: textarea + id: json + attributes: + label: JSON OpenApi + description: Your generated JSON OpenApi documentation (`bin/console nelmio:apidoc:dump`) + value: | +
JSON OpenApi + + ```json + + Replace this text with your JSON (`bin/console nelmio:apidoc:dump`) + + ``` + +
+ validations: + required: false - type: textarea id: additional-context attributes: diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index b8e14f420..6f2414a32 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -20,5 +20,6 @@ file that was distributed with this source code. HEADER ], + 'trailing_comma_in_multiline' => false, ]) ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b0b57d5..5f1a002d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,27 @@ -CHANGELOG -========= +# CHANGELOG + +## 4.32.0 + +* Added support to configure `options` and `serializationContext` via `nelmio_api_doc.models.names`. +* Fixed `serializationContext` not being applied to nested models. + +## 4.31.0 + +* Added support to opt out of JMS serializer usage per endpoint by setting `useJms` in the serializationContext. + ```php + #[OA\Response(response: 200, content: new Model(type: UserDto::class, serializationContext: ["useJms" => false]))] + ``` + +## 4.30.0 +* Create top level OpenApi Tag from Tags top level annotations/attributes + +## 4.25.3 -4.28.1 ------ * Calling `DocumentationExtension::getExtendedType()` has been deprecated in favor of `DocumentationExtension::getExtendedTypes()` to align with the deprecation introduced with `symfony/symfony` version `4.2`. -4.26.0 ------ + +## 4.26.0 + * Add ability to configure UI through configuration ```yaml nelmio_api_doc: @@ -19,8 +34,8 @@ nelmio_api_doc: deepLinking: true ``` -4.25.0 ------ +## 4.25.0 + * Added support for [JMS @Discriminator](https://jmsyst.com/libs/serializer/master/reference/annotations#discriminator) annotation/attribute ```php #[\JMS\Serializer\Annotation\Discriminator(field: 'type', map: ['car' => Car::class, 'plane' => Plane::class])] @@ -29,8 +44,8 @@ nelmio_api_doc: class Plane extends Vehicle { } ``` -4.24.0 ------ +## 4.24.0 + * Added support for some integer ranges (https://phpstan.org/writing-php-code/phpdoc-types#integer-ranges). Annotations attached to integer properties like: ```php @@ -47,8 +62,8 @@ nelmio_api_doc: ### Minor breaking change Dropped support for PHP 7.2 and PHP 7.3. PHP 7.4 is the minimum required version now. -4.23.0 ------ +## 4.23.0 + * Cache configuration option `nelmio_api_doc.cache.item_id` now automatically gets the area appended. ```yml nelmio_api_doc: @@ -101,8 +116,8 @@ Dropped support for PHP 7.2 and PHP 7.3. PHP 7.4 is the minimum required version ``` * Updated nullable enum handling to align with the behaviour of other object types. It now uses wraps nullable enums with `oneOf` instead of `allOf`. -4.22.0 ------ +## 4.22.0 + * Updated bundle directory structure to recommended file structure as described in https://symfony.com/doc/7.0/bundles/best_practices.html. It might be necessary to reinstall the assets: @@ -124,8 +139,8 @@ doc-api: prefix: /api/doc ``` -4.21.0 ------ +## 4.21.0 + * Added bundle configuration options `nelmio_api_doc.cache.pool` and `nelmio_api_doc.cache.item_id`. ```yml nelmio_api_doc: @@ -134,29 +149,30 @@ doc-api: item_id: nelmio_api_doc.docs ``` -4.20.0 ------ +## 4.20.0 + * Added Redocly as an alternative to Swagger UI. https://github.com/Redocly/redoc. * Added support for describing dictionary types in OpenAPI 3.0. -4.17.0 ------ +## 4.17.0 + * Passing groups to `PropertyDescriberInterface::describe()` via the `$groups` parameter is deprecated, the parameter will get removed in a future version. Pass groups via `$context['groups']` instead. -4.0.0 ------ + +## 4.0.0 + * Added support of OpenAPI 3.0. The internals were completely reworked and this version introduces BC breaks. -3.7.0 ------ +## 3.7.0 + * Added `@SerializedName` annotation support and name converters when using Symfony >= 4.2. * Removed pattern added from the Expression Violation message. * Added FOSRestBundle 3.x support * Added `@SWG` annotations support at methods level in models -3.3.0 ------ +## 3.3.0 + * 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. @@ -165,8 +181,7 @@ doc-api: * The Twig template for the Swagger UI now contains blocks to make it easier to overwrite certain parts. See the [official documentation](https://symfony.com/doc/current/bundles/NelmioApiDocBundle/customization.html) how to do this. -3.2.0 (2018-03-24) ------------------- +## 3.2.0 (2018-03-24) * Add a documentation form extension. Use the ``documentation`` option to define how a form field is documented. * Allow references to config definitions in controllers. @@ -202,8 +217,7 @@ Config * Added dependency for "symfony/options-resolver:^3.4.4|^4.0" -3.1.0 (2018-01-28) ------------------- +## 3.1.0 (2018-01-28) * Added Symfony Validator constraints support @@ -234,8 +248,7 @@ Config areas: [ path_patterns: [ /api ] ] ``` -3.0.0 (2017-12-10) ------------------- +## 3.0.0 (2017-12-10) Large refactoring introducing `zircote/swagger-php` for swagger annotations. diff --git a/config/services.xml b/config/services.xml index 3382906f2..be9c3cc56 100644 --- a/config/services.xml +++ b/config/services.xml @@ -139,10 +139,6 @@ - - - - diff --git a/docs/customization.rst b/docs/customization.rst index 27700481c..fbedac427 100644 --- a/docs/customization.rst +++ b/docs/customization.rst @@ -68,4 +68,24 @@ Just create a file ``templates/bundles/NelmioApiDocBundle/SwaggerUi/index.html.t {% endblock javascripts %} -You can have a look at the `original template `_, in ``/Resources/views/SwaggerUi/index.html.twig``, to see which blocks can be overridden. +You can have a look at the `original template `_, in ``/templates/SwaggerUi/index.html.twig``, to see which blocks can be overridden. + +Assets Loading Options +----------------------- + +The `html_config` settings allow you to configure how assets are loaded for the UI. The `assets_mode` option supports three values: `cdn`, `bundle`, and `offline`. + + + .. code-block:: yaml + + nelmio_api_doc: + html_config: + assets_mode: 'cdn' # Other values: 'bundle', 'offline' + +`assets_mode` +~~~~~~~~~~~~~ + +The three values possible values can be found in `AssetsMode.php `_ +- **cdn**: Loads assets from `jsDelivr `_. +- **bundle**: Fetches assets from the bundle in the vendor directory, including updates. +- **offline**: Loads assets from the local `assets` directory, requiring the developer to update them manually. diff --git a/docs/index.rst b/docs/index.rst index c3c529699..eec3812ea 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -109,7 +109,7 @@ It generates an OpenAPI documentation from your Symfony app thanks to routes, etc. If you configured the ``app.swagger_ui`` route above, you can browse your -documentation at `http://example.org/api/doc`. +documentation at ``http://example.org/api/doc``. Using the bundle ---------------- @@ -350,7 +350,7 @@ properties and validator constraints. Take the model class below: } The ``NotBlank`` constraint will apply only to the ``default`` and ``create`` -group, but not ``update``. In more practical terms: the `username` property +group, but not ``update``. In more practical terms: the ``username`` property would show as ``required`` for both model create and default, but not update. When using code generators to build API clients, this often translates into client side validation and types. ``NotBlank`` adding ``required`` will cause @@ -509,6 +509,18 @@ General PHP objects nelmio_api_doc: models: { use_jms: false } + Alternatively, it is also possible to opt out of JMS serializer usage per endpoint by setting ``useJms`` in the serializationContext: + + .. configuration-block:: + + .. code-block:: php-annotations + + /** @OA\Response(response=200, @Model(type=UserDto::class, serializationContext={"useJms"=false})) */ + + .. code-block:: php-attributes + + #[OA\Response(response: 200, content: new Model(type: UserDto::class, serializationContext: ["useJms" => false]))] + When using the JMS serializer combined with `willdurand/Hateoas`_ (and the `BazingaHateoasBundle`_), HATEOAS metadata are automatically extracted diff --git a/docs/symfony_attributes.rst b/docs/symfony_attributes.rst index b874b6af0..fa149a074 100644 --- a/docs/symfony_attributes.rst +++ b/docs/symfony_attributes.rst @@ -197,4 +197,4 @@ Make sure to use at least php 8.1 (attribute support) to make use of this functi .. _`Symfony MapQueryString`: https://symfony.com/doc/current/controller.html#mapping-the-whole-query-string .. _`Symfony MapQueryParameter`: https://symfony.com/doc/current/controller.html#mapping-query-parameters-individually .. _`Symfony MapRequestPayload`: https://symfony.com/doc/current/controller.html#mapping-request-payload -.. _`RouteArgumentDescriberInterface`: https://github.com/DjordyKoert/NelmioApiDocBundle/blob/master/RouteDescriber/RouteArgumentDescriber/RouteArgumentDescriberInterface.php +.. _`RouteArgumentDescriberInterface`: https://github.com/DjordyKoert/NelmioApiDocBundle/blob/master/src/RouteDescriber/RouteArgumentDescriber/RouteArgumentDescriberInterface.php diff --git a/public/init-swagger-ui.js b/public/init-swagger-ui.js index bb06f3f26..8f4ee3c8a 100644 --- a/public/init-swagger-ui.js +++ b/public/init-swagger-ui.js @@ -25,26 +25,52 @@ function loadSwaggerUI(userOptions = {}) { const storageKey = 'nelmio_api_auth'; - // if we have auth in storage use it - if (sessionStorage.getItem(storageKey)) { - try { - ui.authActions.authorize(JSON.parse(sessionStorage.getItem(storageKey))); - } catch (ignored) { - // catch any errors here so it does not stop script execution + function getAuthorizationsFromStorage() { + if (sessionStorage.getItem(storageKey)) { + try { + return JSON.parse(sessionStorage.getItem(storageKey)); + } catch (ignored) { + // catch any errors here so it does not stop script execution + } } + + return {}; + } + + // if we have auth in storage use it + try { + const currentAuthorizations = getAuthorizationsFromStorage(); + Object.keys(currentAuthorizations).forEach(k => ui.authActions.authorize({[k]: currentAuthorizations[k]})); + } catch (ignored) { + // catch any errors here so it does not stop script execution } // hook into authorize to store the auth in local storage when user performs authorization const currentAuthorize = ui.authActions.authorize; ui.authActions.authorize = function (payload) { - sessionStorage.setItem(storageKey, JSON.stringify(payload)); + try { + sessionStorage.setItem(storageKey, JSON.stringify(Object.assign( + getAuthorizationsFromStorage(), + payload + ))); + } catch (ignored) { + // catch any errors here so it does not stop script execution + } + return currentAuthorize(payload); }; // hook into logout to clear auth from storage if user logs out const currentLogout = ui.authActions.logout; ui.authActions.logout = function (payload) { - sessionStorage.removeItem(storageKey); + try { + let currentAuth = getAuthorizationsFromStorage(); + payload.forEach(k => delete currentAuth[k]); + sessionStorage.setItem(storageKey, JSON.stringify(currentAuth)); + } catch (ignored) { + // catch any errors here so it does not stop script execution + } + return currentLogout(payload); }; diff --git a/src/ApiDocGenerator.php b/src/ApiDocGenerator.php index ee100affc..cedcd1614 100644 --- a/src/ApiDocGenerator.php +++ b/src/ApiDocGenerator.php @@ -20,7 +20,6 @@ use OpenApi\Analysis; use OpenApi\Annotations\OpenApi; use OpenApi\Generator; -use OpenApi\Processors\ProcessorInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerAwareTrait; @@ -146,7 +145,7 @@ public function generate(): OpenApi * * @param Generator $generator The generator instance to get the standard processors from * - * @return array The array of processors + * @return array The array of processors */ private function getProcessors(Generator $generator): array { diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index a3bbd5404..0b2ec84fb 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -175,6 +175,17 @@ public function getConfigTreeBuilder(): TreeBuilder ->thenInvalid('Model groups must be either `null` or an array.') ->end() ->end() + ->variableNode('options') + ->defaultValue(null) + ->validate() + ->ifTrue(function ($v) { return null !== $v && !is_array($v); }) + ->thenInvalid('Model options must be either `null` or an array.') + ->end() + ->end() + ->arrayNode('serializationContext') + ->defaultValue([]) + ->prototype('variable')->end() + ->end() ->arrayNode('areas') ->defaultValue([]) ->prototype('scalar')->end() diff --git a/src/DependencyInjection/NelmioApiDocExtension.php b/src/DependencyInjection/NelmioApiDocExtension.php index 4376aa8a1..757703632 100644 --- a/src/DependencyInjection/NelmioApiDocExtension.php +++ b/src/DependencyInjection/NelmioApiDocExtension.php @@ -35,6 +35,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; @@ -42,7 +43,6 @@ use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Routing\RouteCollection; final class NelmioApiDocExtension extends Extension implements PrependExtensionInterface @@ -273,18 +273,6 @@ public function load(array $configs, ContainerBuilder $container): void // Import the base configuration $container->getDefinition('nelmio_api_doc.describers.config')->replaceArgument(0, $config['documentation']); - - // Compatibility Symfony - $controllerNameConverter = null; - if ($container->hasDefinition('.legacy_controller_name_converter')) { // 4.4 - $controllerNameConverter = $container->getDefinition('.legacy_controller_name_converter'); - } elseif ($container->hasDefinition('controller_name_converter')) { // < 4.4 - $controllerNameConverter = $container->getDefinition('controller_name_converter'); - } - - if (null !== $controllerNameConverter) { - $container->getDefinition('nelmio_api_doc.controller_reflector')->setArgument(1, $controllerNameConverter); - } } /** @@ -303,6 +291,8 @@ private function findNameAliases(array $names, string $area): array $aliases[$nameAlias['alias']] = [ 'type' => $nameAlias['type'], 'groups' => $nameAlias['groups'], + 'options' => $nameAlias['options'], + 'serializationContext' => $nameAlias['serializationContext'], ]; } diff --git a/src/Describer/OpenApiPhpDescriber.php b/src/Describer/OpenApiPhpDescriber.php index 14b792938..20cc958de 100644 --- a/src/Describer/OpenApiPhpDescriber.php +++ b/src/Describer/OpenApiPhpDescriber.php @@ -135,6 +135,9 @@ public function describe(OA\OpenApi $api): void $annotation->validate(); $mergeProperties->tags[] = $annotation->name; + $tag = Util::getTag($api, $annotation->name); + $tag->mergeProperties($annotation); + continue; } diff --git a/src/Model/ModelRegistry.php b/src/Model/ModelRegistry.php index a11421b2d..d258d3f14 100644 --- a/src/Model/ModelRegistry.php +++ b/src/Model/ModelRegistry.php @@ -67,7 +67,12 @@ public function __construct($modelDescribers, OA\OpenApi $api, array $alternativ $this->api = $api; $this->logger = new NullLogger(); 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'], + $criteria['options'] ?? null, + $criteria['serializationContext'] ?? [], + ); $this->names[$model->getHash()] = $alternativeName; $this->registeredModelNames[$alternativeName] = $model; Util::getSchema($this->api, $alternativeName); diff --git a/src/ModelDescriber/Annotations/AnnotationsReader.php b/src/ModelDescriber/Annotations/AnnotationsReader.php index df1597263..8eae411bf 100644 --- a/src/ModelDescriber/Annotations/AnnotationsReader.php +++ b/src/ModelDescriber/Annotations/AnnotationsReader.php @@ -42,14 +42,12 @@ public function __construct( ); } - public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): UpdateClassDefinitionResult + public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): bool { $this->openApiAnnotationsReader->updateSchema($reflectionClass, $schema); $this->symfonyConstraintAnnotationReader->setSchema($schema); - return new UpdateClassDefinitionResult( - $this->shouldDescribeModelProperties($schema) - ); + return $this->shouldDescribeModelProperties($schema); } /** @@ -72,9 +70,12 @@ public function updateProperty($reflection, OA\Property $property, ?array $seria } /** - * if an objects schema type and ref are undefined OR the object was manually - * defined as an object, then we're good to do the normal describe flow of - * class properties. + * Whether the model describer should continue reading class properties + * after updating the open api schema from an `OA\Schema` definition. + * + * Users may manually define a `type` or `ref` on a schema, and if that's the case + * model describers should _probably_ not describe any additional properties or try + * to merge in properties. */ private function shouldDescribeModelProperties(OA\Schema $schema): bool { diff --git a/src/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php b/src/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php index 39db78e5e..116958d07 100644 --- a/src/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php +++ b/src/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php @@ -90,6 +90,7 @@ private function processPropertyAnnotations($reflection, OA\Property $property, $existingRequiredFields[] = $propertyName; $this->schema->required = array_values(array_unique($existingRequiredFields)); + $property->nullable = false; } elseif ($annotation instanceof Assert\Length) { if (isset($annotation->min)) { $property->minLength = $annotation->min; diff --git a/src/ModelDescriber/Annotations/UpdateClassDefinitionResult.php b/src/ModelDescriber/Annotations/UpdateClassDefinitionResult.php deleted file mode 100644 index 1cafbd4f2..000000000 --- a/src/ModelDescriber/Annotations/UpdateClassDefinitionResult.php +++ /dev/null @@ -1,41 +0,0 @@ -shouldDescribeModelProperties = $shouldDescribeModelProperties; - } - - public function shouldDescribeModelProperties(): bool - { - return $this->shouldDescribeModelProperties; - } -} diff --git a/src/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php b/src/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php index 3fe03159b..0a83b85e9 100644 --- a/src/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php +++ b/src/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php @@ -54,7 +54,8 @@ protected function applyOpenApiDiscriminator( $oneOfSchema->ref = $modelRegistry->register(new Model( new Type(Type::BUILTIN_TYPE_OBJECT, false, $className), $model->getGroups(), - $model->getOptions() + $model->getOptions(), + $model->getSerializationContext() )); $schema->oneOf[] = $oneOfSchema; $schema->discriminator->mapping[$propertyValue] = $oneOfSchema->ref; diff --git a/src/ModelDescriber/BazingaHateoasModelDescriber.php b/src/ModelDescriber/BazingaHateoasModelDescriber.php index ef7803780..078f1ee18 100644 --- a/src/ModelDescriber/BazingaHateoasModelDescriber.php +++ b/src/ModelDescriber/BazingaHateoasModelDescriber.php @@ -71,7 +71,7 @@ public function describe(Model $model, OA\Schema $schema): void $property = Util::getProperty($relationSchema, $relation->getName()); if (null !== $embedded && method_exists($embedded, 'getType') && null !== $embedded->getType()) { - $this->JMSModelDescriber->describeItem($embedded->getType(), $property, $context); + $this->JMSModelDescriber->describeItem($embedded->getType(), $property, $context, $model->getSerializationContext()); } else { $property->type = 'object'; } diff --git a/src/ModelDescriber/FormModelDescriber.php b/src/ModelDescriber/FormModelDescriber.php index 5ad00753b..7b216755d 100644 --- a/src/ModelDescriber/FormModelDescriber.php +++ b/src/ModelDescriber/FormModelDescriber.php @@ -22,7 +22,6 @@ use OpenApi\Analysis; use OpenApi\Annotations as OA; use OpenApi\Generator; -use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormConfigInterface; use Symfony\Component\Form\FormFactoryInterface; @@ -74,9 +73,6 @@ public function __construct( public function describe(Model $model, OA\Schema $schema): void { - 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.'); - } if (null === $this->formFactory) { throw new \LogicException('You need to enable forms in your application to use a form as a model.'); } @@ -91,7 +87,7 @@ public function describe(Model $model, OA\Schema $schema): void ); $classResult = $annotationsReader->updateDefinition(new \ReflectionClass($class), $schema); - if (!$classResult->shouldDescribeModelProperties()) { + if (!$classResult) { return; } diff --git a/src/ModelDescriber/JMSModelDescriber.php b/src/ModelDescriber/JMSModelDescriber.php index d46f908ed..49da02fb2 100644 --- a/src/ModelDescriber/JMSModelDescriber.php +++ b/src/ModelDescriber/JMSModelDescriber.php @@ -119,7 +119,7 @@ public function describe(Model $model, OA\Schema $schema) ); $classResult = $annotationsReader->updateDefinition(new \ReflectionClass($className), $schema); - if (!$classResult->shouldDescribeModelProperties()) { + if (!$classResult) { return; } $schema->type = 'object'; @@ -152,7 +152,6 @@ public function describe(Model $model, OA\Schema $schema) } catch (\ReflectionException $ignored) { } } - $this->checkRequiredFields($reflections, $schema, $name); if (null !== $item->setter) { try { $reflections[] = new \ReflectionMethod($item->class, $item->setter); @@ -201,7 +200,7 @@ public function describe(Model $model, OA\Schema $schema) continue; } - $this->describeItem($item->type, $property, $context); + $this->describeItem($item->type, $property, $context, $model->getSerializationContext()); $context->popPropertyMetadata(); } $context->popClassMetadata(); @@ -262,6 +261,10 @@ private function computeGroups(Context $context, ?array $type = null): ?array public function supports(Model $model): bool { + if (($model->getSerializationContext()['useJms'] ?? null) === false) { + return false; + } + $className = $model->getType()->getClassName(); try { @@ -278,8 +281,9 @@ public function supports(Model $model): bool * @internal * * @param mixed[] $type + * @param mixed[] $serializationContext */ - public function describeItem(array $type, OA\Schema $property, Context $context): void + public function describeItem(array $type, OA\Schema $property, Context $context, array $serializationContext): void { $nestedTypeInfo = $this->getNestedTypeInArray($type); if (null !== $nestedTypeInfo) { @@ -296,14 +300,14 @@ public function describeItem(array $type, OA\Schema $property, Context $context) return; } - $this->describeItem($nestedType, $property->additionalProperties, $context); + $this->describeItem($nestedType, $property->additionalProperties, $context, $serializationContext); return; } $property->type = 'array'; $property->items = Util::createChild($property, OA\Items::class); - $this->describeItem($nestedType, $property->items, $context); + $this->describeItem($nestedType, $property->items, $context, $serializationContext); } elseif ('array' === $type['name']) { $property->type = 'object'; $property->additionalProperties = true; @@ -330,8 +334,9 @@ public function describeItem(array $type, OA\Schema $property, Context $context) } $groups = $this->computeGroups($context, $type); + unset($serializationContext['groups']); - $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, null, $serializationContext); $modelRef = $this->modelRegistry->register($model); $customFields = (array) $property->jsonSerialize(); @@ -398,34 +403,4 @@ private function propertyTypeUsesGroups(array $type): ?bool return null; } } - - /** - * Mark property as required if it is not nullable. - * - * @param array<\ReflectionProperty|\ReflectionMethod> $reflections - */ - private function checkRequiredFields(array $reflections, OA\Schema $schema, string $name): void - { - foreach ($reflections as $reflection) { - $nullable = false; - if ($reflection instanceof \ReflectionProperty) { - $type = PHP_VERSION_ID >= 70400 ? $reflection->getType() : null; - if (null !== $type && !$type->allowsNull()) { - $nullable = true; - } - } elseif ($reflection instanceof \ReflectionMethod) { - $returnType = $reflection->getReturnType(); - if (null !== $returnType && !$returnType->allowsNull()) { - $nullable = true; - } - } - if ($nullable) { - $required = Generator::UNDEFINED !== $schema->required ? $schema->required : []; - $required[] = $name; - - $schema->required = $required; - break; - } - } - } } diff --git a/src/ModelDescriber/ObjectModelDescriber.php b/src/ModelDescriber/ObjectModelDescriber.php index f119bdef0..f511629b7 100644 --- a/src/ModelDescriber/ObjectModelDescriber.php +++ b/src/ModelDescriber/ObjectModelDescriber.php @@ -92,7 +92,7 @@ public function describe(Model $model, OA\Schema $schema) ); $classResult = $annotationsReader->updateDefinition($reflClass, $schema); - if (!$classResult->shouldDescribeModelProperties()) { + if (!$classResult) { return; } @@ -182,6 +182,8 @@ public function describe(Model $model, OA\Schema $schema) $this->describeProperty($types, $model, $property, $propertyName, $schema); } + + $this->markRequiredProperties($schema); } /** @@ -233,6 +235,36 @@ private function describeProperty(array $types, Model $model, OA\Schema $propert throw new \Exception(sprintf('Type "%s" is not supported in %s::$%s. You may use the `@OA\Property(type="")` annotation to specify it manually.', $types[0]->getBuiltinType(), $model->getType()->getClassName(), $propertyName)); } + /** + * Mark properties as required while ordering them in the same order as the properties of the schema. + * Then append the original required properties. + */ + private function markRequiredProperties(OA\Schema $schema): void + { + if (Generator::isDefault($properties = $schema->properties)) { + return; + } + + $newRequired = []; + foreach ($properties as $property) { + if (is_array($schema->required) && \in_array($property->property, $schema->required, true)) { + $newRequired[] = $property->property; + continue; + } + + if (true === $property->nullable || !Generator::isDefault($property->default)) { + continue; + } + $newRequired[] = $property->property; + } + + if ([] !== $newRequired) { + $originalRequired = Generator::isDefault($schema->required) ? [] : $schema->required; + + $schema->required = array_values(array_unique(array_merge($newRequired, $originalRequired))); + } + } + public function supports(Model $model): bool { return Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType() diff --git a/src/OpenApiPhp/Util.php b/src/OpenApiPhp/Util.php index 9c967b731..75d50ac2b 100644 --- a/src/OpenApiPhp/Util.php +++ b/src/OpenApiPhp/Util.php @@ -70,6 +70,30 @@ public static function getPath(OA\OpenApi $api, string $path): OA\PathItem return self::getIndexedCollectionItem($api, OA\PathItem::class, $path); } + /** + * Return an existing Tag object from $api->tags[] having its member name set to $name. + * Create, add to $api->tags[] and return this new Tag object and set the property if none found. + * + * @see OA\OpenApi::$tags + * @see OA\Tag::$name + */ + public static function getTag(OA\OpenApi $api, string $name): OA\Tag + { + // Tags ar not considered indexed, so we cannot use getIndexedCollectionItem directly + // because we need to specify that the search should use the "name" property. + $key = self::searchIndexedCollectionItem( + is_array($api->tags) ? $api->tags : [], + 'name', + $name + ); + + if (false === $key) { + $key = self::createCollectionItem($api, 'tags', OA\Tag::class, ['name' => $name]); + } + + return $api->tags[$key]; + } + /** * 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. @@ -273,14 +297,20 @@ public static function searchCollectionItem(array $collection, array $properties /** * Search for an Annotation within the $collection that has its member $index set to $value. * - * @param mixed[] $collection - * @param mixed $value The value to search for + * @param OA\AbstractAnnotation[] $collection + * @param mixed $value The value to search for * * @return false|int|string */ public static function searchIndexedCollectionItem(array $collection, string $member, $value) { - return array_search($value, array_column($collection, $member), true); + foreach ($collection as $i => $child) { + if ($child->{$member} === $value) { + return $i; + } + } + + return false; } /** diff --git a/src/Processor/MapQueryStringProcessor.php b/src/Processor/MapQueryStringProcessor.php index 273b4e0f5..79223aa93 100644 --- a/src/Processor/MapQueryStringProcessor.php +++ b/src/Processor/MapQueryStringProcessor.php @@ -18,7 +18,6 @@ use OpenApi\Analysis; use OpenApi\Annotations as OA; use OpenApi\Generator; -use OpenApi\Processors\ProcessorInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; /** @@ -27,7 +26,7 @@ * * @see SymfonyMapQueryStringDescriber */ -final class MapQueryStringProcessor implements ProcessorInterface +final class MapQueryStringProcessor { public function __invoke(Analysis $analysis): void { diff --git a/src/Processor/MapRequestPayloadProcessor.php b/src/Processor/MapRequestPayloadProcessor.php index ea0f41c22..351baf393 100644 --- a/src/Processor/MapRequestPayloadProcessor.php +++ b/src/Processor/MapRequestPayloadProcessor.php @@ -18,7 +18,6 @@ use OpenApi\Analysis; use OpenApi\Annotations as OA; use OpenApi\Generator; -use OpenApi\Processors\ProcessorInterface; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -28,7 +27,7 @@ * * @see SymfonyMapRequestPayloadDescriber */ -final class MapRequestPayloadProcessor implements ProcessorInterface +final class MapRequestPayloadProcessor { public function __invoke(Analysis $analysis): void { diff --git a/src/Processor/NullablePropertyProcessor.php b/src/Processor/NullablePropertyProcessor.php index cf3db55b4..a46f90a03 100644 --- a/src/Processor/NullablePropertyProcessor.php +++ b/src/Processor/NullablePropertyProcessor.php @@ -16,12 +16,11 @@ use OpenApi\Analysis; use OpenApi\Annotations as OA; use OpenApi\Generator; -use OpenApi\Processors\ProcessorInterface; /** * Processor to clean up the generated OpenAPI documentation for nullable properties. */ -final class NullablePropertyProcessor implements ProcessorInterface +final class NullablePropertyProcessor { public function __invoke(Analysis $analysis): void { diff --git a/src/PropertyDescriber/RequiredPropertyDescriber.php b/src/PropertyDescriber/RequiredPropertyDescriber.php index fe61e4f2b..f4d88fc41 100644 --- a/src/PropertyDescriber/RequiredPropertyDescriber.php +++ b/src/PropertyDescriber/RequiredPropertyDescriber.php @@ -16,6 +16,8 @@ /** * Mark a property as required if it is not nullable. + * + * @deprecated {@see ObjectModelDescriber::markRequiredProperties()} */ final class RequiredPropertyDescriber implements PropertyDescriberInterface, PropertyDescriberAwareInterface { diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 6e4d87a02..fd3a9275f 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -113,6 +113,21 @@ public function testAlternativeNames(): void 'type' => 'App\Foo', 'groups' => ['group1', ['group2', 'parent' => 'child3']], ], + [ + 'alias' => 'Foo1', + 'type' => 'App\Foo', + 'options' => null, + ], + [ + 'alias' => 'Foo1', + 'type' => 'App\Foo', + 'options' => ['foo' => 'bar'], + ], + [ + 'alias' => 'Foo1', + 'type' => 'App\Foo', + 'serializationContext' => ['useJms' => false, 'foo' => 'bar'], + ], ], ], ]]); @@ -121,36 +136,75 @@ public function testAlternativeNames(): void 'alias' => 'Foo1', 'type' => 'App\Foo', 'groups' => ['group'], + 'options' => null, + 'serializationContext' => [], 'areas' => [], ], [ 'alias' => 'Foo2', 'type' => 'App\Foo', 'groups' => [], + 'options' => null, + 'serializationContext' => [], 'areas' => [], ], [ 'alias' => 'Foo3', 'type' => 'App\Foo', 'groups' => null, + 'options' => null, + 'serializationContext' => [], 'areas' => [], ], [ 'alias' => 'Foo4', 'type' => 'App\\Foo', 'groups' => ['group'], + 'options' => null, + 'serializationContext' => [], 'areas' => ['internal'], ], [ 'alias' => 'Foo1', 'type' => 'App\\Foo', 'groups' => null, + 'options' => null, + 'serializationContext' => [], 'areas' => ['internal'], ], [ 'alias' => 'Foo1', 'type' => 'App\Foo', 'groups' => ['group1', ['group2', 'parent' => 'child3']], + 'options' => null, + 'serializationContext' => [], + 'areas' => [], + ], + [ + 'alias' => 'Foo1', + 'type' => 'App\Foo', + 'groups' => null, + 'options' => null, + 'serializationContext' => [], + 'areas' => [], + ], + [ + 'alias' => 'Foo1', + 'type' => 'App\Foo', + 'groups' => null, + 'options' => ['foo' => 'bar'], + 'serializationContext' => [], + 'areas' => [], + ], + [ + 'alias' => 'Foo1', + 'type' => 'App\Foo', + 'groups' => null, + 'options' => null, + 'serializationContext' => [ + 'useJms' => false, + 'foo' => 'bar', + ], 'areas' => [], ], ], $config['models']['names']); @@ -222,5 +276,20 @@ public static function provideInvalidConfiguration(): \Generator ], 'Model groups must be either `null` or an array.', ]; + + yield 'invalid options value for model' => [ + [ + 'models' => [ + 'names' => [ + [ + 'alias' => 'Foo1', + 'type' => 'App\Foo', + 'options' => 'invalid_string_value', + ], + ], + ], + ], + 'Model options must be either `null` or an array.', + ]; } } diff --git a/tests/DependencyInjection/NelmioApiDocExtensionTest.php b/tests/DependencyInjection/NelmioApiDocExtensionTest.php index 1e37bcf9d..4c9bd891a 100644 --- a/tests/DependencyInjection/NelmioApiDocExtensionTest.php +++ b/tests/DependencyInjection/NelmioApiDocExtensionTest.php @@ -56,10 +56,14 @@ public function testNameAliasesArePassedToModelRegistry(): void 'Foo1' => [ 'type' => 'App\\Foo', 'groups' => null, + 'options' => null, + 'serializationContext' => [], ], 'Test1' => [ 'type' => 'App\\Test', 'groups' => null, + 'options' => null, + 'serializationContext' => [], ], ], $methodCall[1][0]); $foundMethodCall = true; @@ -75,10 +79,14 @@ public function testNameAliasesArePassedToModelRegistry(): void 'Foo1' => [ 'type' => 'App\\Bar', 'groups' => null, + 'options' => null, + 'serializationContext' => [], ], 'Test1' => [ 'type' => 'App\\Test', 'groups' => null, + 'options' => null, + 'serializationContext' => [], ], ], $methodCall[1][0]); $foundMethodCall = true; diff --git a/tests/Functional/Configs/AlternativeNamesPHP80Entities.yaml b/tests/Functional/Configs/AlternativeNamesPHP80Entities.yaml index 783aea0d3..748b1cd13 100644 --- a/tests/Functional/Configs/AlternativeNamesPHP80Entities.yaml +++ b/tests/Functional/Configs/AlternativeNamesPHP80Entities.yaml @@ -12,3 +12,5 @@ services: OpenApi\Processors\CleanUnusedComponents: tags: - { name: 'nelmio_api_doc.swagger.processor', priority: -100 } + calls: + - setEnabled: [ true ] \ No newline at end of file diff --git a/tests/Functional/Configs/AlternativeNamesPHP81Entities.yaml b/tests/Functional/Configs/AlternativeNamesPHP81Entities.yaml index b81d5d994..81c10e7a3 100644 --- a/tests/Functional/Configs/AlternativeNamesPHP81Entities.yaml +++ b/tests/Functional/Configs/AlternativeNamesPHP81Entities.yaml @@ -12,3 +12,5 @@ services: OpenApi\Processors\CleanUnusedComponents: tags: - { name: 'nelmio_api_doc.swagger.processor', priority: -100 } + calls: + - setEnabled: [ true ] diff --git a/tests/Functional/Configs/CleanUnusedComponentsProcessor.yaml b/tests/Functional/Configs/CleanUnusedComponentsProcessor.yaml index ff03db6fc..d487a00c9 100644 --- a/tests/Functional/Configs/CleanUnusedComponentsProcessor.yaml +++ b/tests/Functional/Configs/CleanUnusedComponentsProcessor.yaml @@ -2,3 +2,5 @@ services: OpenApi\Processors\CleanUnusedComponents: tags: - { name: 'nelmio_api_doc.swagger.processor', priority: -100 } + calls: + - setEnabled: [ true ] \ No newline at end of file diff --git a/tests/Functional/Configs/JMS.yaml b/tests/Functional/Configs/JMS.yaml new file mode 100644 index 000000000..37b65232b --- /dev/null +++ b/tests/Functional/Configs/JMS.yaml @@ -0,0 +1,3 @@ +nelmio_api_doc: + models: + use_jms: true diff --git a/tests/Functional/Controller/JMSController80.php b/tests/Functional/Controller/JMSController80.php index 3f99afa37..0f9f57d8b 100644 --- a/tests/Functional/Controller/JMSController80.php +++ b/tests/Functional/Controller/JMSController80.php @@ -16,7 +16,6 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex80; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSDualComplex; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSNamingStrategyConstraints; -use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped80; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSUser; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChat; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatRoomUser; @@ -167,18 +166,4 @@ public function minUserNestedAction() public function discriminatorMapAction() { } - - /** - * @Route("/api/jms_typed", methods={"GET"}) - * - * @OA\Response( - * response=200, - * description="Success", - * - * @Model(type=JMSTyped80::class) - * ) - */ - public function typedAction() - { - } } diff --git a/tests/Functional/Controller/JMSController81.php b/tests/Functional/Controller/JMSController81.php index a367e2209..8848e196a 100644 --- a/tests/Functional/Controller/JMSController81.php +++ b/tests/Functional/Controller/JMSController81.php @@ -17,7 +17,6 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex81; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSDualComplex; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSNamingStrategyConstraints; -use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped81; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSUser; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChat; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatRoomUser; @@ -142,14 +141,4 @@ public function enum() public function discriminatorMapAction() { } - - #[Route('/api/jms_typed', methods: ['GET'])] - #[OA\Response( - response: 200, - description: 'Success', - content: new Model(type: JMSTyped81::class)) - ] - public function typedAction() - { - } } diff --git a/tests/Functional/Controller/JmsOptOutController.php b/tests/Functional/Controller/JmsOptOutController.php new file mode 100644 index 000000000..a138e9748 --- /dev/null +++ b/tests/Functional/Controller/JmsOptOutController.php @@ -0,0 +1,41 @@ + false]) + )] + public function jmsOptOut() + { + } +} diff --git a/tests/Functional/Controller/OpenApiTagController.php b/tests/Functional/Controller/OpenApiTagController.php new file mode 100644 index 000000000..735ca3fd3 --- /dev/null +++ b/tests/Functional/Controller/OpenApiTagController.php @@ -0,0 +1,33 @@ +configurableContainerFactory = new ConfigurableContainerFactory(); - - static::createClient([], ['HTTP_HOST' => 'api.example.com']); - } - - /** - * @param array $options - */ - protected static function createKernel(array $options = []): KernelInterface - { - return new NelmioKernel([], null, []); } protected function getOpenApiDefinition(string $area = 'default'): OA\OpenApi @@ -53,9 +44,10 @@ protected function getOpenApiDefinition(string $area = 'default'): OA\OpenApi * @dataProvider provideUniversalTestCases * * @param array{name: string, type: string}|null $controller + * @param Bundle[] $extraBundles * @param string[] $extraConfigs */ - public function testControllers(?array $controller, ?string $fixtureName = null, array $extraConfigs = []): void + public function testControllers(?array $controller, ?string $fixtureName = null, array $extraBundles = [], array $extraConfigs = []): void { $controllerName = $controller['name'] ?? null; $controllerType = $controller['type'] ?? null; @@ -70,7 +62,7 @@ public function testControllers(?array $controller, ?string $fixtureName = null, $routes->withPath('/')->import(__DIR__."/Controller/$controllerName.php", $controllerType); }; - $this->configurableContainerFactory->create([], $routingConfiguration, $extraConfigs); + $this->configurableContainerFactory->create($extraBundles, $routingConfiguration, $extraConfigs); $apiDefinition = $this->getOpenApiDefinition(); @@ -99,9 +91,20 @@ public static function provideAttributeTestCases(): \Generator 'type' => $type, ], 'PromotedPropertiesDefaults', + [], [__DIR__.'/Configs/AlternativeNamesPHP81Entities.yaml'], ]; + yield 'JMS model opt out' => [ + [ + 'name' => 'JmsOptOutController', + 'type' => $type, + ], + 'JmsOptOutController', + [new JMSSerializerBundle()], + [__DIR__.'/Configs/JMS.yaml'], + ]; + if (version_compare(Kernel::VERSION, '6.3.0', '>=')) { yield 'https://github.com/nelmio/NelmioApiDocBundle/issues/2209' => [ [ @@ -121,6 +124,7 @@ public static function provideAttributeTestCases(): \Generator 'type' => $type, ], 'MapQueryStringCleanupComponents', + [], [__DIR__.'/Configs/CleanUnusedComponentsProcessor.yaml'], ]; @@ -145,6 +149,13 @@ public static function provideAttributeTestCases(): \Generator ], ]; + yield 'Create top level Tag from Tag attribute' => [ + [ + 'name' => 'OpenApiTagController', + 'type' => $type, + ], + ]; + if (property_exists(MapRequestPayload::class, 'type')) { yield 'Symfony 7.1 MapRequestPayload array type' => [ [ @@ -169,6 +180,7 @@ public static function provideAnnotationTestCases(): \Generator 'type' => 'annotation', ], 'PromotedPropertiesDefaults', + [], [__DIR__.'/Configs/AlternativeNamesPHP80Entities.yaml'], ]; } @@ -182,6 +194,7 @@ public static function provideUniversalTestCases(): \Generator yield 'https://github.com/nelmio/NelmioApiDocBundle/issues/2224' => [ null, 'VendorExtension', + [], [__DIR__.'/Configs/VendorExtension.yaml'], ]; } diff --git a/tests/Functional/Entity/JMSTyped80.php b/tests/Functional/Entity/JMSTyped80.php deleted file mode 100644 index 9602dbeac..000000000 --- a/tests/Functional/Entity/JMSTyped80.php +++ /dev/null @@ -1,46 +0,0 @@ - 'User', 'required' => [ + 'email', + 'location', + 'friendsNumber', 'creationDate', 'users', 'status', diff --git a/tests/Functional/JMSFunctionalTest.php b/tests/Functional/JMSFunctionalTest.php index 1b3a598ed..fb671d3b4 100644 --- a/tests/Functional/JMSFunctionalTest.php +++ b/tests/Functional/JMSFunctionalTest.php @@ -360,12 +360,6 @@ public function testEnumSupport(): void ], ], 'schema' => 'Article81', - 'required' => [ - 'id', - 'type', - 'int_backed_type', - 'not_backed_type', - ], ], json_decode($this->getModel('Article81')->toJson(), true)); self::assertEquals([ @@ -426,23 +420,4 @@ protected static function createKernel(array $options = []): KernelInterface { return new TestKernel(TestKernel::USE_JMS); } - - public function testModelTypedDocumentation(): void - { - self::assertEquals([ - 'type' => 'object', - 'properties' => [ - 'id' => ['type' => 'integer'], - 'user' => ['$ref' => '#/components/schemas/JMSUser'], - 'name' => ['type' => 'string'], - 'virtual_friend' => ['$ref' => '#/components/schemas/JMSUser'], - ], - 'required' => [ - 'virtual_friend', - 'id', - 'user', - ], - 'schema' => 'JMSTyped', - ], json_decode($this->getModel('JMSTyped')->toJson(), true)); - } } diff --git a/tests/Functional/SwaggerPHPApiComplianceTest.php b/tests/Functional/SwaggerPHPApiComplianceTest.php index bedb2aa47..36bfae580 100644 --- a/tests/Functional/SwaggerPHPApiComplianceTest.php +++ b/tests/Functional/SwaggerPHPApiComplianceTest.php @@ -13,6 +13,7 @@ use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Analysis; +use OpenApi\Context; class SwaggerPHPApiComplianceTest extends WebTestCase { @@ -30,7 +31,12 @@ public function testAllContextsCopyRoot(): void self::assertTrue($root->is('version')); foreach ((new Analysis([$openApi], Util::createContext()))->annotations as $annotation) { - self::assertSame($annotation->_context->version, $root->version); + /* @phpstan-ignore function.alreadyNarrowedType */ + if (method_exists(Context::class, 'getVersion')) { + self::assertSame($annotation->_context->getVersion(), $root->getVersion()); + } else { + self::assertSame($annotation->_context->version, $root->version); + } } } } diff --git a/tests/Functional/TestKernel.php b/tests/Functional/TestKernel.php index 2f5e94dc1..2e6d96b08 100644 --- a/tests/Functional/TestKernel.php +++ b/tests/Functional/TestKernel.php @@ -22,8 +22,6 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\BazingaUser; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex80; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex81; -use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped80; -use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped81; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture; use Nelmio\ApiDocBundle\Tests\Functional\Entity\PrivateProtectedExposure; use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups; @@ -223,10 +221,6 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'type' => JMSComplex80::class, 'groups' => null, ], - [ - 'alias' => 'JMSTyped', - 'type' => JMSTyped80::class, - ], ]); } elseif (self::isAttributesAvailable()) { $models = array_merge($models, [ @@ -244,10 +238,6 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'type' => JMSComplex81::class, 'groups' => null, ], - [ - 'alias' => 'JMSTyped', - 'type' => JMSTyped81::class, - ], ]); } diff --git a/tests/Functional/ValidationGroupsFunctionalTest.php b/tests/Functional/ValidationGroupsFunctionalTest.php index ad522968b..2cc94b84c 100644 --- a/tests/Functional/ValidationGroupsFunctionalTest.php +++ b/tests/Functional/ValidationGroupsFunctionalTest.php @@ -35,6 +35,7 @@ public function testConstraintGroupsAreRespectedWhenDescribingModels(): void $expected = [ 'required' => [ 'property', + 'propertyNotNullOnSpecificGroup', ], 'properties' => [ 'property' => [ @@ -42,6 +43,9 @@ public function testConstraintGroupsAreRespectedWhenDescribingModels(): void // the min/max constraint is in the default group only and shouldn't // be read here with validation groups turned on ], + 'propertyNotNullOnSpecificGroup' => [ + 'type' => 'string', + ], ], 'type' => 'object', 'schema' => 'SymfonyConstraintsTestGroup', @@ -75,12 +79,17 @@ public function testConstraintDefaultGroupsAreRespectedWhenReadingAnnotations(): 'type' => 'string', ], ], + 'propertyNotNullOnSpecificGroup' => [ + 'type' => 'string', + 'nullable' => true, + ], ], 'type' => 'object', 'schema' => 'SymfonyConstraintsDefaultGroup', 'required' => [ 'property', 'propertyInDefaultGroup', + 'propertyArray', ], ]; diff --git a/tests/ModelDescriber/Annotations/AnnotationReaderTest.php b/tests/ModelDescriber/Annotations/AnnotationReaderTest.php index 785670177..4057bd329 100644 --- a/tests/ModelDescriber/Annotations/AnnotationReaderTest.php +++ b/tests/ModelDescriber/Annotations/AnnotationReaderTest.php @@ -56,7 +56,7 @@ class_exists(AnnotationReader::class) ? new AnnotationReader() : null, public static function provideProperty(): \Generator { - yield 'Annotations' => [new class() { + yield 'Annotations' => [new class { /** * @OA\Property(example=1) */ @@ -68,7 +68,7 @@ public static function provideProperty(): \Generator }]; if (\PHP_VERSION_ID >= 80100) { - yield 'Attributes' => [new class() { + yield 'Attributes' => [new class { #[OAattr\Property(example: 1)] public $property1; #[OAattr\Property(example: 'some example', description: 'some description')] diff --git a/tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php b/tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php index 94741734e..d1f03e2c5 100644 --- a/tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php +++ b/tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php @@ -39,7 +39,7 @@ protected function setUp(): void public function testUpdatePropertyFix1283(): void { if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { - $entity = new class() { + $entity = new class { /** * @Assert\NotBlank() * @@ -53,7 +53,7 @@ public function testUpdatePropertyFix1283(): void public $property2; }; } else { - $entity = new class() { + $entity = new class { #[Assert\Length(min: 1)] #[Assert\NotBlank()] public $property1; @@ -106,7 +106,7 @@ public static function provideOptionalProperty(): \Generator { if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { yield 'Annotations' => [ - new class() { + new class { /** * @Assert\NotBlank(allowNull = true) * @@ -123,7 +123,7 @@ public static function provideOptionalProperty(): \Generator } if (\PHP_VERSION_ID >= 80000) { - yield 'Attributes' => [new class() { + yield 'Attributes' => [new class { #[Assert\NotBlank(allowNull: true)] #[Assert\Length(min: 1)] public $property1; @@ -161,7 +161,7 @@ public static function provideAssertChoiceResultsInNumericArray(): \Generator if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { yield 'Annotations' => [ - new class() { + new class { /** * @Assert\Length(min = 1) * @@ -173,7 +173,7 @@ public static function provideAssertChoiceResultsInNumericArray(): \Generator } if (\PHP_VERSION_ID >= 80000) { - yield 'Attributes' => [new class() { + yield 'Attributes' => [new class { #[Assert\Length(min: 1)] #[Assert\Choice(choices: TEST_ASSERT_CHOICE_STATUSES)] public $property1; @@ -203,7 +203,7 @@ public function testMultipleChoiceConstraintsApplyEnumToItems($entity): void public static function provideMultipleChoiceConstraintsApplyEnumToItems(): \Generator { if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { - yield 'Annotations' => [new class() { + yield 'Annotations' => [new class { /** * @Assert\Choice(choices={"one", "two"}, multiple=true) */ @@ -212,7 +212,7 @@ public static function provideMultipleChoiceConstraintsApplyEnumToItems(): \Gene } if (\PHP_VERSION_ID >= 80000) { - yield 'Attributes' => [new class() { + yield 'Attributes' => [new class { #[Assert\Choice(choices: ['one', 'two'], multiple: true)] public $property1; }]; @@ -244,7 +244,7 @@ public static function provideLengthConstraintDoesNotSetMaxLengthIfMaxIsNotSet() { if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { yield 'Annotations' => [ - new class() { + new class { /** * @Assert\Length(min = 1) */ @@ -254,7 +254,7 @@ public static function provideLengthConstraintDoesNotSetMaxLengthIfMaxIsNotSet() } if (\PHP_VERSION_ID >= 80000) { - yield 'Attributes' => [new class() { + yield 'Attributes' => [new class { #[Assert\Length(min: 1)] public $property1; }]; @@ -286,7 +286,7 @@ public static function provideLengthConstraintDoesNotSetMinLengthIfMinIsNotSet() { if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { yield 'Annotations' => [ - new class() { + new class { /** * @Assert\Length(max = 100) */ @@ -296,7 +296,7 @@ public static function provideLengthConstraintDoesNotSetMinLengthIfMinIsNotSet() } if (\PHP_VERSION_ID >= 80000) { - yield 'Attributes' => [new class() { + yield 'Attributes' => [new class { #[Assert\Length(max: 100)] public $property1; }]; @@ -306,14 +306,14 @@ public static function provideLengthConstraintDoesNotSetMinLengthIfMinIsNotSet() public function testCompoundValidationRules(): void { if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { - $entity = new class() { + $entity = new class { /** * @CustomAssert\CompoundValidationRule() */ public $property1; }; } else { - $entity = new class() { + $entity = new class { #[CustomAssert\CompoundValidationRule()] public $property1; }; @@ -368,7 +368,7 @@ public static function provideCountConstraintDoesNotSetMinItemsIfMinIsNotSet(): { if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { yield 'Annotations' => [ - new class() { + new class { /** * @Assert\Count(max = 10) */ @@ -378,7 +378,7 @@ public static function provideCountConstraintDoesNotSetMinItemsIfMinIsNotSet(): } if (\PHP_VERSION_ID >= 80000) { - yield 'Attributes' => [new class() { + yield 'Attributes' => [new class { #[Assert\Count(max: 10)] public $property1; }]; @@ -410,7 +410,7 @@ public static function provideCountConstraintDoesNotSetMaxItemsIfMaxIsNotSet(): { if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { yield 'Annotations' => [ - new class() { + new class { /** * @Assert\Count(min = 10) */ @@ -420,7 +420,7 @@ public static function provideCountConstraintDoesNotSetMaxItemsIfMaxIsNotSet(): } if (\PHP_VERSION_ID >= 80000) { - yield 'Attributes' => [new class() { + yield 'Attributes' => [new class { #[Assert\Count(min: 10)] public $property1; }]; @@ -452,7 +452,7 @@ public static function provideRangeConstraintDoesNotSetMaximumIfMaxIsNotSet(): \ { if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { yield 'Annotations' => [ - new class() { + new class { /** * @Assert\Range(min = 10) */ @@ -462,7 +462,7 @@ public static function provideRangeConstraintDoesNotSetMaximumIfMaxIsNotSet(): \ } if (\PHP_VERSION_ID >= 80000) { - yield 'Attributes' => [new class() { + yield 'Attributes' => [new class { #[Assert\Range(min: 10)] public $property1; }]; @@ -494,7 +494,7 @@ public static function provideRangeConstraintDoesNotSetMinimumIfMinIsNotSet(): \ { if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { yield 'Annotations' => [ - new class() { + new class { /** * @Assert\Range(max = 10) */ @@ -504,7 +504,7 @@ public static function provideRangeConstraintDoesNotSetMinimumIfMinIsNotSet(): \ } if (\PHP_VERSION_ID >= 80000) { - yield 'Attributes' => [new class() { + yield 'Attributes' => [new class { #[Assert\Range(max: 10)] public $property1; }]; @@ -612,7 +612,7 @@ public function testReaderWithValidationGroupsEnabledCanReadFromMultipleValidati public static function provideConstraintsWithGroups(): \Generator { if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { - yield 'Annotations' => [new class() { + yield 'Annotations' => [new class { /** * @Assert\NotBlank() * @@ -623,7 +623,7 @@ public static function provideConstraintsWithGroups(): \Generator } if (\PHP_VERSION_ID >= 80000) { - yield 'Attributes' => [new class() { + yield 'Attributes' => [new class { #[Assert\NotBlank()] #[Assert\Range(min: 1, groups: ['other'])] public $property1; diff --git a/tests/SwaggerPhp/UtilTest.php b/tests/SwaggerPhp/UtilTest.php index f45699059..2510b2664 100644 --- a/tests/SwaggerPhp/UtilTest.php +++ b/tests/SwaggerPhp/UtilTest.php @@ -866,6 +866,24 @@ public static function provideMergeData(): \Generator ]; } + public function testGetTag(): void + { + $api = self::createObj(OA\OpenApi::class, ['_context' => new Context()]); + self::assertEquals(Generator::UNDEFINED, $api->tags); + + $tag = Util::getTag($api, 'foo'); + self::assertEquals('foo', $tag->name); + self::assertEquals(Generator::UNDEFINED, $tag->description); + self::assertEquals(Generator::UNDEFINED, $tag->externalDocs); + + self::assertIsArray($api->tags); + + $api->tags[] = self::createObj(OA\Tag::class, ['name' => 'bar', 'description' => 'baz']); + $tag = Util::getTag($api, 'bar'); + self::assertEquals('bar', $tag->name); + self::assertEquals('baz', $tag->description); + } + public function assertIsNested(OA\AbstractAnnotation $parent, OA\AbstractAnnotation $child): void { self::assertTrue($child->_context->is('nested'));