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'));