diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 000000000..dc7dae788 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,47 @@ +name: Backport merged pull request + +on: + pull_request_target: + types: + - closed + - labeled + +permissions: + contents: write # so it can comment + pull-requests: write # so it can create pull requests + +jobs: + backport: + name: Backport pull request + runs-on: ubuntu-latest + if: > + ( + github.event.action == 'closed' && + github.event.pull_request.merged + ) || + ( + github.event.action == 'labeled' + && startsWith(github.event.label.name, 'backport') + && github.event.pull_request.merged + ) + steps: + - uses: actions/create-github-app-token@v2 + id: generate-token + with: + app-id: ${{ secrets.BACKPORT_APP_ID }} + private-key: ${{ secrets.BACKPORT_APP_PRIVATE_KEY }} + - uses: actions/checkout@v4 + - name: Create backport pull requests + uses: korthout/backport-action@v3 + with: + # Token to authenticate requests to GitHub, ensures that created PRs run CI + github_token: ${{ steps.generate-token.outputs.token }} + branch_name: backport-#${pull_number}-to-${target_branch} + pull_description: | + ### This is an automated backport of #${pull_number} to branch `${target_branch}`. + + > [!CAUTION] + > **Do not modify this pull request.** + + ${pull_description} + pull_title: "${pull_title} [Backport #${pull_number} to ${target_branch}]" \ No newline at end of file diff --git a/.github/workflows/release.backport.yml b/.github/workflows/release.backport.yml new file mode 100644 index 000000000..78e8619af --- /dev/null +++ b/.github/workflows/release.backport.yml @@ -0,0 +1,25 @@ +name: Release Backport +on: + push: + branches: + - 4.x + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4.4.0 + with: + # this assumes that you have created a personal access token + # (PAT) and configured it as a GitHub action secret named + # `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important). + token: ${{ secrets.GITHUB_TOKEN }} + # this is a built-in strategy in release-please, see "Action Inputs" + # for more options + release-type: php + versioning-strategy: always-bump-patch + target-branch: 4.x diff --git a/CHANGELOG.md b/CHANGELOG.md index bb4318adc..acaeaf208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # CHANGELOG +## [4.38.5](https://github.com/nelmio/NelmioApiDocBundle/compare/v4.38.4...v4.38.5) (2025-11-14) + + +### Bug Fixes + +* **configuration:** validate `type_info` option on Symfony 6 ([#2615](https://github.com/nelmio/NelmioApiDocBundle/issues/2615)) ([b625aa6](https://github.com/nelmio/NelmioApiDocBundle/commit/b625aa6cba40d3125bcd2c3abb0c7361350bf6f8)) + +## [4.38.4](https://github.com/nelmio/NelmioApiDocBundle/compare/v4.38.3...v4.38.4) (2025-11-13) + + +### Bug Fixes + +* swagger-php 5.7.0 compatatiblity [Backport [#2598](https://github.com/nelmio/NelmioApiDocBundle/issues/2598) to 4.x] ([#2603](https://github.com/nelmio/NelmioApiDocBundle/issues/2603)) ([ef3ba7d](https://github.com/nelmio/NelmioApiDocBundle/commit/ef3ba7d303cba97d5db995597d416d9ab0451c63)) +* **swagger-php:** conflict with broken version 5.5.0 [Backport [#2568](https://github.com/nelmio/NelmioApiDocBundle/issues/2568) to 4.x] ([#2604](https://github.com/nelmio/NelmioApiDocBundle/issues/2604)) ([82e24b4](https://github.com/nelmio/NelmioApiDocBundle/commit/82e24b463c3253d04e29370a401539b80f52935e)) + ## 4.38.2 - Support of attribute MapQueryParameter with a regexp has been improved, it now converts the regexp from PCRE to ECMA-262 for better compliance with OpenApi. diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index a76ca9b0c..263e52670 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -20,7 +20,7 @@ Here are some additional advices that are more likely to apply to NelmioApiDocBu - Upgrade all your ``use Swagger\Annotations as SWG`` statements to ``use OpenApi\Annotations as OA;`` (to simplify the upgrade you may also stick to the ``SWG`` aliasing). In case you changed ``SWG`` to ``OA``, upgrade all your annotations from ``@SWG\...`` to ``@OA\...``. -- Update your config in case you used inlined swagger documentation (the field ``nelmio_api_doc.documentation``). [A tool](https://openapi-converter.herokuapp.com/) is available to help you convert it. +- Update your config in case you used inlined swagger documentation (the field ``nelmio_api_doc.documentation``). [A tool](https://converter.swagger.io/) is available to help you convert it. - In case you used ``@OA\Response(..., @OA\Schema(...))``, you should explicit your media type by using the annotation ``@OA\JsonContent`` or ``@OA\XmlContent`` instead of ``@OA\Schema``: ``@OA\Response(..., @OA\JsonContent(...))`` or ``@OA\Response(..., @OA\XmlContent(...))``. diff --git a/composer.json b/composer.json index 6912ca64a..b8930b882 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,12 @@ { "name": "nelmio/api-doc-bundle", "description": "Generates documentation for your REST API from annotations and attributes", - "keywords": ["api", "documentation", "doc", "rest"], + "keywords": [ + "api", + "documentation", + "doc", + "rest" + ], "type": "symfony-bundle", "license": "MIT", "authors": [ @@ -61,7 +66,7 @@ "willdurand/hateoas-bundle": "^1.0 || ^2.0" }, "conflict": { - "zircote/swagger-php": "4.8.7" + "zircote/swagger-php": "4.8.7 || 5.5.0" }, "suggest": { "api-platform/core": "For using an API oriented framework.", @@ -89,7 +94,12 @@ } }, "config": { - "sort-packages": true + "sort-packages": true, + "audit": { + "ignore": [ + "PKSA-365x-2zjk-pt47" + ] + } }, "extra": { "branch-alias": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b9f3a3e92..d4d795d31 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,11 @@ parameters: ignoreErrors: + - + message: '#^Call to function method_exists\(\) with ''Symfony\\\\Component\\\\PropertyInfo\\\\PropertyInfoExtractor'' and ''getType'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: src/DependencyInjection/Configuration.php + - message: "#^Class Nelmio\\\\ApiDocBundle\\\\Annotation\\\\Areas extends @final class Nelmio\\\\ApiDocBundle\\\\Attribute\\\\Areas\\.$#" count: 1 diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 7be17d93d..2a4688255 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -14,6 +14,7 @@ use Nelmio\ApiDocBundle\Render\Html\AssetsMode; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; final class Configuration implements ConfigurationInterface { @@ -28,6 +29,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->booleanNode('type_info') ->info('Use the symfony/type-info component for determining types.') ->defaultFalse() + ->validate() + ->ifTrue(static fn ($v) => true === $v && !method_exists(PropertyInfoExtractor::class, 'getType')) + ->thenInvalid('the type_info option requires Symfony 7 or higher. Please upgrade Symfony or set type_info to false.') + ->end() ->end() ->booleanNode('use_validation_groups') ->info('If true, `groups` passed to @Model annotations will be used to limit validation constraints') diff --git a/src/Describer/DefaultDescriber.php b/src/Describer/DefaultDescriber.php index b69120cf1..c4efc1d72 100644 --- a/src/Describer/DefaultDescriber.php +++ b/src/Describer/DefaultDescriber.php @@ -42,10 +42,17 @@ public function describe(OA\OpenApi $api): void foreach (Util::OPERATIONS as $method) { /** @var OA\Operation $operation */ $operation = $path->{$method}; - if (Generator::UNDEFINED !== $operation && null !== $operation && (Generator::UNDEFINED === $operation->responses || [] === $operation->responses)) { - /** @var OA\Response $response */ - $response = Util::getIndexedCollectionItem($operation, OA\Response::class, 'default'); - $response->description = ''; + if (Generator::UNDEFINED !== $operation && null !== $operation) { + $hasAnyResponses = false; + if (Generator::UNDEFINED !== $operation->responses && [] !== $operation->responses && null !== $operation->responses) { + $hasAnyResponses = true; + } + + if (!$hasAnyResponses) { + /** @var OA\Response $response */ + $response = Util::getIndexedCollectionItem($operation, OA\Response::class, 'default'); + $response->description = ''; + } } } } diff --git a/src/Describer/OpenApiPhpDescriber.php b/src/Describer/OpenApiPhpDescriber.php index 898d1334d..77540028e 100644 --- a/src/Describer/OpenApiPhpDescriber.php +++ b/src/Describer/OpenApiPhpDescriber.php @@ -103,6 +103,20 @@ public function describe(OA\OpenApi $api): void continue; } + if ($annotation instanceof \OpenApi\Attributes\Post || + $annotation instanceof \OpenApi\Attributes\Get || + $annotation instanceof \OpenApi\Attributes\Put || + $annotation instanceof \OpenApi\Attributes\Delete || + $annotation instanceof \OpenApi\Attributes\Patch) { + $className = get_class($annotation); + $methodName = strtolower(substr($className, strrpos($className, "\\") + 1)); + if (in_array($methodName, $httpMethods, true)) { + $operation = Util::getOperation($path, $methodName); + $operation->mergeProperties($annotation); + } + continue; + } + if ($annotation instanceof OA\Operation) { if (!in_array($annotation->method, $httpMethods, true)) { continue; diff --git a/src/Processor/MapQueryStringProcessor.php b/src/Processor/MapQueryStringProcessor.php index 79223aa93..090669506 100644 --- a/src/Processor/MapQueryStringProcessor.php +++ b/src/Processor/MapQueryStringProcessor.php @@ -85,6 +85,7 @@ private function addQueryParameters(Analysis $analysis, OA\Operation $operation, // Remove incompatible properties $propertyVars = get_object_vars($property); unset($propertyVars['property']); + unset($propertyVars['encoding']); $schema = new OA\Schema($propertyVars); diff --git a/src/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php b/src/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php index 0c49e464d..da3ceb2f6 100644 --- a/src/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php +++ b/src/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php @@ -16,14 +16,11 @@ use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; use OpenApi\Generator; -use OpenApi\Processors\Concerns\TypesTrait; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; final class SymfonyMapQueryParameterDescriber implements RouteArgumentDescriberInterface { - use TypesTrait; - public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $operation): void { if (!$attribute = $argumentMetadata->getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) {