From 55f27dc7adbf2456ec7700dd57e10c036eb260c0 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Mon, 27 May 2024 11:47:26 +0200 Subject: [PATCH 1/3] fix(symfony): documentation request _format (#6390) fixes #6372 --- features/openapi/docs.feature | 6 +++ src/Metadata/Util/ContentNegotiationTrait.php | 16 +++++++- .../Serializer/SerializerContextBuilder.php | 38 +++++++++++++++++++ src/OpenApi/State/OpenApiProvider.php | 34 +++++++++++++++++ src/State/Provider/DeserializeProvider.php | 11 ++++-- src/State/Provider/ReadProvider.php | 5 ++- src/State/Tests/Provider/ReadProviderTest.php | 2 +- src/Symfony/Action/DocumentationAction.php | 7 +--- .../Bundle/Resources/config/openapi.xml | 11 ++++++ .../EventListener/DeserializeListener.php | 11 ++++-- src/Symfony/EventListener/ReadListener.php | 5 ++- tests/.ignored-deprecations-legacy-events | 2 +- .../Provider/DeserializeProviderTest.php | 2 +- 13 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 src/OpenApi/Serializer/SerializerContextBuilder.php create mode 100644 src/OpenApi/State/OpenApiProvider.php diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature index 1a5e61c1513..980b896959a 100644 --- a/features/openapi/docs.feature +++ b/features/openapi/docs.feature @@ -432,3 +432,9 @@ Feature: Documentation support ] """ And the JSON node "components.schemas.DummyBoolean.properties.isDummyBoolean.owl:maxCardinality" should not exist + + Scenario: Retrieve the OpenAPI documentation in JSON + Given I add "Accept" header equal to "text/html,*/*;q=0.8" + And I send a "GET" request to "/docs.jsonopenapi" + Then the response status code should be 200 + And the response should be in JSON diff --git a/src/Metadata/Util/ContentNegotiationTrait.php b/src/Metadata/Util/ContentNegotiationTrait.php index c86b2cc3c28..e2cecb80d3c 100644 --- a/src/Metadata/Util/ContentNegotiationTrait.php +++ b/src/Metadata/Util/ContentNegotiationTrait.php @@ -106,7 +106,7 @@ private function getRequestFormat(Request $request, array $formats, bool $throw } } - // Then use the Symfony request format if available and applicable + // Then, use the Symfony request format if available and applicable $requestFormat = $request->getRequestFormat('') ?: null; if (null !== $requestFormat) { $mimeType = $request->getMimeType($requestFormat); @@ -135,4 +135,18 @@ private function getNotAcceptableHttpException(string $accept, array $mimeTypes) implode('", "', array_keys($mimeTypes)) )); } + + /** + * Adds the supported formats to the request. + * + * This is necessary for {@see Request::getMimeType} and {@see Request::getMimeTypes} to work. + * + * @param array $formats + */ + private function addRequestFormats(Request $request, array $formats): void + { + foreach ($formats as $format => $mimeTypes) { + $request->setFormat($format, (array) $mimeTypes); + } + } } diff --git a/src/OpenApi/Serializer/SerializerContextBuilder.php b/src/OpenApi/Serializer/SerializerContextBuilder.php new file mode 100644 index 00000000000..afb48243639 --- /dev/null +++ b/src/OpenApi/Serializer/SerializerContextBuilder.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\OpenApi\Serializer; + +use ApiPlatform\State\SerializerContextBuilderInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * @internal + */ +final class SerializerContextBuilder implements SerializerContextBuilderInterface +{ + public function __construct(private readonly SerializerContextBuilderInterface $decorated) + { + } + + public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array + { + $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); + + return $context + [ + 'api_gateway' => $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY), + 'base_url' => $request->getBaseUrl(), + 'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION), + ]; + } +} diff --git a/src/OpenApi/State/OpenApiProvider.php b/src/OpenApi/State/OpenApiProvider.php new file mode 100644 index 00000000000..1aeebbcf012 --- /dev/null +++ b/src/OpenApi/State/OpenApiProvider.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\OpenApi\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; +use ApiPlatform\OpenApi\OpenApi; +use ApiPlatform\State\ProviderInterface; + +/** + * @internal + */ +final class OpenApiProvider implements ProviderInterface +{ + public function __construct(private readonly OpenApiFactoryInterface $openApiFactory) + { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): OpenApi + { + return $this->openApiFactory->__invoke($context); + } +} diff --git a/src/State/Provider/DeserializeProvider.php b/src/State/Provider/DeserializeProvider.php index bea57d63c5b..18db103f3bc 100644 --- a/src/State/Provider/DeserializeProvider.php +++ b/src/State/Provider/DeserializeProvider.php @@ -15,8 +15,9 @@ use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operation; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\Serializer\SerializerContextBuilderInterface as LegacySerializerContextBuilderInterface; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; use ApiPlatform\Validator\Exception\ValidationException; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -32,8 +33,12 @@ final class DeserializeProvider implements ProviderInterface { - public function __construct(private readonly ?ProviderInterface $decorated, private readonly SerializerInterface $serializer, private readonly SerializerContextBuilderInterface $serializerContextBuilder, private ?TranslatorInterface $translator = null) - { + public function __construct( + private readonly ?ProviderInterface $decorated, + private readonly SerializerInterface $serializer, + private readonly LegacySerializerContextBuilderInterface|SerializerContextBuilderInterface $serializerContextBuilder, + private ?TranslatorInterface $translator = null + ) { if (null === $this->translator) { $this->translator = new class() implements TranslatorInterface, LocaleAwareInterface { use TranslatorTrait; diff --git a/src/State/Provider/ReadProvider.php b/src/State/Provider/ReadProvider.php index d9c92794251..c854dfc011c 100644 --- a/src/State/Provider/ReadProvider.php +++ b/src/State/Provider/ReadProvider.php @@ -17,9 +17,10 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Util\CloneTrait; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\Serializer\SerializerContextBuilderInterface as LegacySerializerContextBuilderInterface; use ApiPlatform\State\Exception\ProviderNotFoundException; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; use ApiPlatform\State\UriVariablesResolverTrait; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; use ApiPlatform\State\Util\RequestParser; @@ -38,7 +39,7 @@ final class ReadProvider implements ProviderInterface public function __construct( private readonly ProviderInterface $provider, - private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null, + private readonly LegacySerializerContextBuilderInterface|SerializerContextBuilderInterface|null $serializerContextBuilder = null, ) { } diff --git a/src/State/Tests/Provider/ReadProviderTest.php b/src/State/Tests/Provider/ReadProviderTest.php index 0823819feca..3299b0ab447 100644 --- a/src/State/Tests/Provider/ReadProviderTest.php +++ b/src/State/Tests/Provider/ReadProviderTest.php @@ -14,9 +14,9 @@ namespace ApiPlatform\State\Tests\Provider; use ApiPlatform\Metadata\Get; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; use ApiPlatform\State\Provider\ReadProvider; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Symfony/Action/DocumentationAction.php b/src/Symfony/Action/DocumentationAction.php index 64b26c6ec68..c66cc8e202e 100644 --- a/src/Symfony/Action/DocumentationAction.php +++ b/src/Symfony/Action/DocumentationAction.php @@ -67,6 +67,7 @@ public function __invoke(?Request $request = null) 'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION), ]; $request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context); + $this->addRequestFormats($request, $this->documentationFormats); $format = $this->getRequestFormat($request, $this->documentationFormats); if (null !== $this->openApiFactory && ('html' === $format || OpenApiNormalizer::FORMAT === $format || OpenApiNormalizer::JSON_FORMAT === $format || OpenApiNormalizer::YAML_FORMAT === $format)) { @@ -87,11 +88,7 @@ private function getOpenApiDocumentation(array $context, string $format, Request class: OpenApi::class, read: true, serialize: true, - provider: fn () => $this->openApiFactory->__invoke($context), - normalizationContext: [ - ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null, - LegacyOpenApiNormalizer::SPEC_VERSION => $context['spec_version'] ?? null, - ], + provider: 'api_platform.openapi.provider', outputFormats: $this->documentationFormats ); diff --git a/src/Symfony/Bundle/Resources/config/openapi.xml b/src/Symfony/Bundle/Resources/config/openapi.xml index 57e0e3f48cb..25a7a495529 100644 --- a/src/Symfony/Bundle/Resources/config/openapi.xml +++ b/src/Symfony/Bundle/Resources/config/openapi.xml @@ -29,6 +29,17 @@ + + + + + + + + + + + %api_platform.title% %api_platform.description% diff --git a/src/Symfony/EventListener/DeserializeListener.php b/src/Symfony/EventListener/DeserializeListener.php index c64076404eb..a98f95ec748 100644 --- a/src/Symfony/EventListener/DeserializeListener.php +++ b/src/Symfony/EventListener/DeserializeListener.php @@ -16,8 +16,9 @@ use ApiPlatform\Api\FormatMatcher; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\Serializer\SerializerContextBuilderInterface as LegacySerializerContextBuilderInterface; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; use ApiPlatform\Symfony\Util\RequestAttributesExtractor; use ApiPlatform\Symfony\Validator\Exception\ValidationException; @@ -48,8 +49,12 @@ final class DeserializeListener private SerializerInterface $serializer; private ?ProviderInterface $provider = null; - public function __construct(ProviderInterface|SerializerInterface $serializer, private readonly SerializerContextBuilderInterface|ResourceMetadataCollectionFactoryInterface|null $serializerContextBuilder = null, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?TranslatorInterface $translator = null) - { + public function __construct( + ProviderInterface|SerializerInterface $serializer, + private readonly LegacySerializerContextBuilderInterface|SerializerContextBuilderInterface|ResourceMetadataCollectionFactoryInterface|null $serializerContextBuilder = null, + ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, + private ?TranslatorInterface $translator = null + ) { if ($serializer instanceof ProviderInterface) { $this->provider = $serializer; } else { diff --git a/src/Symfony/EventListener/ReadListener.php b/src/Symfony/EventListener/ReadListener.php index 375aa18dba5..a7a33fb865b 100644 --- a/src/Symfony/EventListener/ReadListener.php +++ b/src/Symfony/EventListener/ReadListener.php @@ -22,11 +22,12 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\UriVariablesConverterInterface; use ApiPlatform\Metadata\Util\CloneTrait; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\Serializer\SerializerContextBuilderInterface as LegacySerializerContextBuilderInterface; use ApiPlatform\State\CallableProvider; use ApiPlatform\State\Exception\ProviderNotFoundException; use ApiPlatform\State\Provider\ReadProvider; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; use ApiPlatform\State\UriVariablesResolverTrait; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; use ApiPlatform\State\Util\RequestParser; @@ -48,7 +49,7 @@ final class ReadListener public function __construct( private readonly ProviderInterface $provider, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, - private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null, + private readonly LegacySerializerContextBuilderInterface|SerializerContextBuilderInterface|null $serializerContextBuilder = null, LegacyUriVariablesConverterInterface|UriVariablesConverterInterface|null $uriVariablesConverter = null, ) { $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; diff --git a/tests/.ignored-deprecations-legacy-events b/tests/.ignored-deprecations-legacy-events index 5dcd9cdd7d5..799c2910648 100644 --- a/tests/.ignored-deprecations-legacy-events +++ b/tests/.ignored-deprecations-legacy-events @@ -19,7 +19,7 @@ %Since api-platform/core 3.3: Use a "ApiPlatform\\State\\ProviderInterface" as first argument in "ApiPlatform\\Symfony\\EventListener\\AddFormatListener" instead of "Negotiation\\Negotiator".% -%Since api-platform/core 3.3: Use a "ApiPlatform\\Metadata\\Resource\\Factory\\ResourceMetadataCollectionFactoryInterface" as second argument in "ApiPlatform\\Symfony\\EventListener\\DeserializeListener" instead of "ApiPlatform\\Serializer\\SerializerContextBuilderInterface".% +%Since api-platform/core 3.3: Use a "ApiPlatform\\Metadata\\Resource\\Factory\\ResourceMetadataCollectionFactoryInterface" as second argument in "ApiPlatform\\Symfony\\EventListener\\DeserializeListener" instead of "ApiPlatform\\State\\SerializerContextBuilderInterface".% # Fixed when ApiPlatform\Api\FilterLocatorTrait will we deleted %ApiPlatform\\Api\\FilterInterface is deprecated in favor of ApiPlatform\\Metadata\\FilterInterface% diff --git a/tests/State/Provider/DeserializeProviderTest.php b/tests/State/Provider/DeserializeProviderTest.php index b94c7473ca9..4d2a8b48075 100644 --- a/tests/State/Provider/DeserializeProviderTest.php +++ b/tests/State/Provider/DeserializeProviderTest.php @@ -14,9 +14,9 @@ namespace ApiPlatform\Tests\State\Provider; use ApiPlatform\Metadata\Post; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; use ApiPlatform\State\Provider\DeserializeProvider; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; From 42ab205369b3439017f8fea45cdaaec1e8cfbd6f Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Tue, 28 May 2024 23:30:48 +0200 Subject: [PATCH 2/3] test: constraint Url needs "requireTld" (#6393) --- tests/.ignored-deprecations | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/.ignored-deprecations b/tests/.ignored-deprecations index 2774847dfcd..d0669f59096 100644 --- a/tests/.ignored-deprecations +++ b/tests/.ignored-deprecations @@ -12,3 +12,5 @@ %ApiPlatform\\Api\\FilterInterface is deprecated in favor of ApiPlatform\\Metadata\\FilterInterface% %The "Symfony\\Bundle\\MakerBundle\\Maker\\MakeAuthenticator" class is deprecated, use any of the Security\\Make\* commands instead% + +%Since symfony/validator 7.1: Not passing a value for the "requireTld" option to the Url constraint is deprecated. Its default value will change to "true".% From b5a93fb0bb855273aabb0807505ba61b68813246 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 29 May 2024 07:48:43 +0200 Subject: [PATCH 3/3] doc: changelog 3.3.5 [ci skip] --- .commitlintrc | 2 +- CHANGELOG.md | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.commitlintrc b/.commitlintrc index 2bb4a45a99b..73d886db1db 100644 --- a/.commitlintrc +++ b/.commitlintrc @@ -84,7 +84,7 @@ "build", "chore", "ci", - "docs", + "doc", "feat", "fix", "perf", diff --git a/CHANGELOG.md b/CHANGELOG.md index 3105fdfaf07..1a76bdf9cae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v3.3.5 + +### Bug fixes + +* [55f27dc7a](https://github.com/api-platform/core/commit/55f27dc7adbf2456ec7700dd57e10c036eb260c0) fix(symfony): documentation request _format (#6390) + ## v3.3.4 ### Bug fixes @@ -2310,4 +2316,4 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link ## 1.0.0 beta 2 * Preserve indexes when normalizing and denormalizing associative arrays -* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance +* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance \ No newline at end of file