Skip to content

Commit 2141b01

Browse files
soyukaSarahshr
andauthored
feat: deprecate not setting formats manually (#5808)
introduces documentation formats Co-authored-by: Sarahshr <[email protected]>
1 parent 2dd058a commit 2141b01

File tree

16 files changed

+237
-29
lines changed

16 files changed

+237
-29
lines changed

features/openapi/docs.feature

+41
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,44 @@ Feature: Documentation support
321321
]
322322
}
323323
"""
324+
325+
Scenario: Retrieve the JSON OpenAPI documentation
326+
Given I add "Accept" header equal to "application/vnd.openapi+json"
327+
And I send a "GET" request to "/docs"
328+
Then the response status code should be 200
329+
And the response should be in JSON
330+
And the header "Content-Type" should be equal to "application/vnd.openapi+json; charset=utf-8"
331+
# Context
332+
And the JSON node "openapi" should be equal to "3.1.0"
333+
# Root properties
334+
And the JSON node "info.title" should be equal to "My Dummy API"
335+
And the JSON node "info.description" should contain "This is a test API."
336+
And the JSON node "info.description" should contain "Made with love"
337+
# Security Schemes
338+
And the JSON node "components.securitySchemes" should be equal to:
339+
"""
340+
{
341+
"oauth": {
342+
"type": "oauth2",
343+
"description": "OAuth 2.0 implicit Grant",
344+
"flows": {
345+
"implicit": {
346+
"authorizationUrl": "http://my-custom-server/openid-connect/auth",
347+
"scopes": {}
348+
}
349+
}
350+
},
351+
"Some_Authorization_Name": {
352+
"type": "apiKey",
353+
"description": "Value for the Authorization header parameter.",
354+
"name": "Authorization",
355+
"in": "header"
356+
}
357+
}
358+
"""
359+
360+
Scenario: Retrieve the YAML OpenAPI documentation
361+
Given I add "Accept" header equal to "application/vnd.openapi+yaml"
362+
And I send a "GET" request to "/docs"
363+
Then the response status code should be 200
364+
And the header "Content-Type" should be equal to "application/vnd.openapi+yaml; charset=utf-8"

src/Documentation/Action/DocumentationAction.php

+9-4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
2222
use ApiPlatform\OpenApi\OpenApi;
2323
use ApiPlatform\OpenApi\Serializer\ApiGatewayNormalizer;
24+
use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer;
2425
use ApiPlatform\State\ProcessorInterface;
2526
use ApiPlatform\State\ProviderInterface;
2627
use Negotiation\Negotiator;
@@ -44,7 +45,8 @@ public function __construct(
4445
private readonly ?OpenApiFactoryInterface $openApiFactory = null,
4546
private readonly ?ProviderInterface $provider = null,
4647
private readonly ?ProcessorInterface $processor = null,
47-
Negotiator $negotiator = null
48+
Negotiator $negotiator = null,
49+
private readonly array $documentationFormats = [OpenApiNormalizer::JSON_FORMAT => ['application/vnd.openapi+json'], OpenApiNormalizer::FORMAT => ['application/json']]
4850
) {
4951
$this->negotiator = $negotiator ?? new Negotiator();
5052
}
@@ -60,9 +62,9 @@ public function __invoke(Request $request = null)
6062

6163
$context = ['api_gateway' => $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY), 'base_url' => $request->getBaseUrl()];
6264
$request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context);
63-
$format = $this->getRequestFormat($request, ['json' => ['application/json'], 'jsonld' => ['application/ld+json'], 'html' => ['text/html']]);
65+
$format = $this->getRequestFormat($request, $this->documentationFormats);
6466

65-
if (null !== $this->openApiFactory && ('html' === $format || 'json' === $format)) {
67+
if (null !== $this->openApiFactory && ('html' === $format || OpenApiNormalizer::FORMAT === $format || OpenApiNormalizer::JSON_FORMAT === $format || OpenApiNormalizer::YAML_FORMAT === $format)) {
6668
return $this->getOpenApiDocumentation($context, $format, $request);
6769
}
6870

@@ -76,10 +78,13 @@ private function getOpenApiDocumentation(array $context, string $format, Request
7678
{
7779
if ($this->provider && $this->processor) {
7880
$context['request'] = $request;
79-
$operation = new Get(class: OpenApi::class, provider: fn () => $this->openApiFactory->__invoke($context), normalizationContext: [ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null]);
81+
$operation = new Get(class: OpenApi::class, provider: fn () => $this->openApiFactory->__invoke($context), normalizationContext: [ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null], outputFormats: $this->documentationFormats);
8082
if ('html' === $format) {
8183
$operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true);
8284
}
85+
if ('json' === $format) {
86+
trigger_deprecation('api-platform/core', '3.2', 'The "json" format is too broad, use "jsonopenapi" instead.');
87+
}
8388

8489
return $this->processor->process($this->provider->provide($operation, [], $context), $operation, [], $context);
8590
}

src/OpenApi/Serializer/OpenApiNormalizer.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
final class OpenApiNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
2727
{
2828
public const FORMAT = 'json';
29+
public const JSON_FORMAT = 'jsonopenapi';
30+
public const YAML_FORMAT = 'yamlopenapi';
2931
private const EXTENSION_PROPERTIES_KEY = 'extensionProperties';
3032

3133
public function __construct(private readonly NormalizerInterface $decorated)
@@ -72,12 +74,12 @@ private function recursiveClean(array $data): array
7274
*/
7375
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
7476
{
75-
return self::FORMAT === $format && $data instanceof OpenApi;
77+
return (self::FORMAT === $format || self::JSON_FORMAT === $format || self::YAML_FORMAT === $format) && $data instanceof OpenApi;
7678
}
7779

7880
public function getSupportedTypes($format): array
7981
{
80-
return self::FORMAT === $format ? [OpenApi::class => true] : [];
82+
return (self::FORMAT === $format || self::JSON_FORMAT === $format || self::YAML_FORMAT === $format) ? [OpenApi::class => true] : [];
8183
}
8284

8385
public function hasCacheableSupportsMethod(): bool

src/OpenApi/Tests/Serializer/ApiGatewayNormalizerTest.php

+3-9
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
use Prophecy\PhpUnit\ProphecyTrait;
2424
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
2525
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
26-
use Symfony\Component\Serializer\Serializer;
2726

2827
final class ApiGatewayNormalizerTest extends TestCase
2928
{
@@ -35,19 +34,14 @@ final class ApiGatewayNormalizerTest extends TestCase
3534
public function testSupportsNormalization(): void
3635
{
3736
$normalizerProphecy = $this->prophesize(NormalizerInterface::class);
37+
$normalizerProphecy->willImplement(CacheableSupportsMethodInterface::class);
3838
$normalizerProphecy->supportsNormalization(OpenApiNormalizer::FORMAT, OpenApi::class)->willReturn(true);
39-
if (!method_exists(Serializer::class, 'getSupportedTypes')) {
40-
$normalizerProphecy->willImplement(CacheableSupportsMethodInterface::class);
41-
$normalizerProphecy->hasCacheableSupportsMethod()->willReturn(true);
42-
}
39+
$normalizerProphecy->hasCacheableSupportsMethod()->willReturn(true);
4340

4441
$normalizer = new ApiGatewayNormalizer($normalizerProphecy->reveal());
4542

4643
$this->assertTrue($normalizer->supportsNormalization(OpenApiNormalizer::FORMAT, OpenApi::class));
47-
48-
if (!method_exists(Serializer::class, 'getSupportedTypes')) {
49-
$this->assertTrue($normalizer->hasCacheableSupportsMethod());
50-
}
44+
$this->assertTrue($normalizer->hasCacheableSupportsMethod());
5145
}
5246

5347
public function testNormalize(): void
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Serializer\Tests;
15+
16+
use ApiPlatform\Serializer\YamlEncoder;
17+
use PHPUnit\Framework\TestCase;
18+
19+
class YamlEncoderTest extends TestCase
20+
{
21+
private YamlEncoder $encoder;
22+
23+
protected function setUp(): void
24+
{
25+
$this->encoder = new YamlEncoder('yamlopenapi');
26+
}
27+
28+
public function testSupportEncoding(): void
29+
{
30+
$this->assertTrue($this->encoder->supportsEncoding('yamlopenapi'));
31+
$this->assertFalse($this->encoder->supportsEncoding('json'));
32+
}
33+
34+
public function testEncode(): void
35+
{
36+
$data = ['foo' => 'bar'];
37+
38+
$this->assertSame('{ foo: bar }', $this->encoder->encode($data, 'yamlopenapi'));
39+
}
40+
41+
public function testSupportDecoding(): void
42+
{
43+
$this->assertTrue($this->encoder->supportsDecoding('yamlopenapi'));
44+
$this->assertFalse($this->encoder->supportsDecoding('json'));
45+
}
46+
47+
public function testDecode(): void
48+
{
49+
$this->assertEquals(['foo' => 'bar'], $this->encoder->decode('{ foo: bar }', 'yamlopenapi'));
50+
}
51+
52+
public function testUTF8EncodedString(): void
53+
{
54+
$data = ['foo' => 'Über'];
55+
56+
$this->assertEquals('{ foo: Über }', $this->encoder->encode($data, 'yamlopenapi'));
57+
}
58+
}

src/Serializer/YamlEncoder.php

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Serializer;
15+
16+
use Symfony\Component\Serializer\Encoder\DecoderInterface;
17+
use Symfony\Component\Serializer\Encoder\EncoderInterface;
18+
use Symfony\Component\Serializer\Encoder\YamlEncoder as BaseYamlEncoder;
19+
20+
/**
21+
* A YAML encoder with appropriate default options to embed the generated document into HTML.
22+
*/
23+
final class YamlEncoder implements EncoderInterface, DecoderInterface
24+
{
25+
public function __construct(private readonly string $format = 'yamlopenapi', private readonly EncoderInterface&DecoderInterface $yamlEncoder = new BaseYamlEncoder())
26+
{
27+
}
28+
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function supportsEncoding($format, array $context = []): bool
33+
{
34+
return $this->format === $format;
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function encode($data, $format, array $context = []): string
41+
{
42+
return $this->yamlEncoder->encode($data, $format, $context);
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public function supportsDecoding($format, array $context = []): bool
49+
{
50+
return $this->format === $format;
51+
}
52+
53+
/**
54+
* {@inheritdoc}
55+
*/
56+
public function decode($data, $format, array $context = []): mixed
57+
{
58+
return $this->yamlEncoder->decode($data, $format, $context);
59+
}
60+
}

src/Serializer/composer.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@
3030
"symfony/validator": "^6.3"
3131
},
3232
"require-dev": {
33+
"api-platform/symfony": "*@dev || ^3.1",
3334
"phpspec/prophecy-phpunit": "^2.0",
34-
"symfony/phpunit-bridge": "^6.1",
3535
"symfony/mercure-bundle": "*",
36-
"api-platform/symfony": "*@dev || ^3.1"
36+
"symfony/phpunit-bridge": "^6.1",
37+
"symfony/yaml": "^6.3"
3738
},
3839
"autoload": {
3940
"psr-4": {

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

+20-3
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,19 @@ public function load(array $configs, ContainerBuilder $container): void
105105
$configuration = new Configuration();
106106
$config = $this->processConfiguration($configuration, $configs);
107107

108+
if (!$config['formats']) {
109+
trigger_deprecation('api-platform/core', '3.2', 'Setting the "formats" section will be mandatory in API Platform 4.');
110+
$config['formats'] = [
111+
'jsonld' => ['mime_types' => ['application/ld+json']],
112+
// Note that in API Platform 4 this will be removed as it was used for documentation only and are is now present in the docsFormats
113+
'json' => ['mime_types' => ['application/json']], // Swagger support
114+
];
115+
}
116+
108117
$formats = $this->getFormats($config['formats']);
109118
$patchFormats = $this->getFormats($config['patch_formats']);
110119
$errorFormats = $this->getFormats($config['error_formats']);
120+
$docsFormats = $this->getFormats($config['docs_formats']);
111121

112122
if (!isset($errorFormats['html']) && $config['enable_swagger'] && $config['enable_swagger_ui']) {
113123
$errorFormats['html'] = ['text/html'];
@@ -122,7 +132,12 @@ public function load(array $configs, ContainerBuilder $container): void
122132
$patchFormats['jsonapi'] = ['application/vnd.api+json'];
123133
}
124134

125-
$this->registerCommonConfiguration($container, $config, $loader, $formats, $patchFormats, $errorFormats);
135+
if (isset($docsFormats['json']) && !isset($docsFormats['jsonopenapi'])) {
136+
trigger_deprecation('api-platform/core', '3.2', 'The "json" format is too broad, use ["jsonopenapi" => ["application/vnd.openapi+json"]] instead.');
137+
$docsFormats['jsonopenapi'] = ['application/vnd.openapi+json'];
138+
}
139+
140+
$this->registerCommonConfiguration($container, $config, $loader, $formats, $patchFormats, $errorFormats, $docsFormats);
126141
$this->registerMetadataConfiguration($container, $config, $loader);
127142
$this->registerOAuthConfiguration($container, $config);
128143
$this->registerOpenApiConfiguration($container, $config, $loader);
@@ -159,7 +174,7 @@ public function load(array $configs, ContainerBuilder $container): void
159174
$this->registerInflectorConfiguration($config);
160175
}
161176

162-
private function registerCommonConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader, array $formats, array $patchFormats, array $errorFormats): void
177+
private function registerCommonConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader, array $formats, array $patchFormats, array $errorFormats, array $docsFormats): void
163178
{
164179
$loader->load('symfony/events.xml');
165180
$loader->load('symfony/controller.xml');
@@ -191,6 +206,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array
191206
$container->setParameter('api_platform.formats', $formats);
192207
$container->setParameter('api_platform.patch_formats', $patchFormats);
193208
$container->setParameter('api_platform.error_formats', $errorFormats);
209+
$container->setParameter('api_platform.docs_formats', $docsFormats);
194210
$container->setParameter('api_platform.eager_loading.enabled', $this->isConfigEnabled($container, $config['eager_loading']));
195211
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);
196212
$container->setParameter('api_platform.eager_loading.fetch_partial', $config['eager_loading']['fetch_partial']);
@@ -286,7 +302,8 @@ private function registerMetadataConfiguration(ContainerBuilder $container, arra
286302

287303
if (!empty($config['resource_class_directories'])) {
288304
$container->setParameter('api_platform.resource_class_directories', array_merge(
289-
$config['resource_class_directories'], $container->getParameter('api_platform.resource_class_directories')
305+
$config['resource_class_directories'],
306+
$container->getParameter('api_platform.resource_class_directories')
290307
));
291308
}
292309

src/Symfony/Bundle/DependencyInjection/Configuration.php

+7-3
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,17 @@ public function getConfigTreeBuilder(): TreeBuilder
157157
$this->addExceptionToStatusSection($rootNode);
158158

159159
$this->addFormatSection($rootNode, 'formats', [
160-
'jsonld' => ['mime_types' => ['application/ld+json']],
161-
'json' => ['mime_types' => ['application/json']], // Swagger support
162-
'html' => ['mime_types' => ['text/html']], // Swagger UI support
163160
]);
164161
$this->addFormatSection($rootNode, 'patch_formats', [
165162
'json' => ['mime_types' => ['application/merge-patch+json']],
166163
]);
164+
$this->addFormatSection($rootNode, 'docs_formats', [
165+
'jsonopenapi' => ['mime_types' => ['application/vnd.openapi+json']],
166+
'yamlopenapi' => ['mime_types' => ['application/vnd.openapi+yaml']],
167+
'json' => ['mime_types' => ['application/json']], // this is only for legacy reasons, use jsonopenapi instead
168+
'jsonld' => ['mime_types' => ['application/ld+json']],
169+
'html' => ['mime_types' => ['text/html']],
170+
]);
167171
$this->addFormatSection($rootNode, 'error_formats', [
168172
'jsonproblem' => ['mime_types' => ['application/problem+json']],
169173
'jsonld' => ['mime_types' => ['application/ld+json']],

src/Symfony/Bundle/Resources/config/api.xml

+2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@
110110
<argument type="service" id="api_platform.openapi.factory" on-invalid="null" />
111111
<argument type="service" id="api_platform.state_provider.main" on-invalid="null" />
112112
<argument type="service" id="api_platform.state_processor.main" on-invalid="null" />
113+
<argument type="service" id="api_platform.negotiator" on-invalid="null" />
114+
<argument>%api_platform.docs_formats%</argument>
113115
</service>
114116

115117
<service id="api_platform.action.exception" class="ApiPlatform\Action\ExceptionAction" public="true">

src/Symfony/Bundle/Resources/config/legacy/events.xml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
1010
<argument>%api_platform.formats%</argument>
1111
<argument>%api_platform.error_formats%</argument>
12+
<argument>%api_platform.docs_formats%</argument>
1213

1314
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="28" />
1415
</service>

src/Symfony/Bundle/Resources/config/openapi.xml

+14
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@
7575
<argument type="service" id="api_platform.pagination_options" />
7676
<argument type="service" id="api_platform.router" />
7777
</service>
78+
79+
<service id="api_platform.jsonopenapi.encoder" class="ApiPlatform\Serializer\JsonEncoder" public="false">
80+
<argument>jsonopenapi</argument>
81+
<argument type="service" id="serializer.json.encoder" on-invalid="null" />
82+
83+
<tag name="serializer.encoder" />
84+
</service>
85+
86+
<service id="api_platform.yamlopenapi.encoder" class="ApiPlatform\Serializer\YamlEncoder" public="false">
87+
<argument>yamlopenapi</argument>
88+
<argument type="service" id="serializer.encoder.yaml" on-invalid="null" />
89+
90+
<tag name="serializer.encoder" />
91+
</service>
7892
</services>
7993

8094
</container>

0 commit comments

Comments
 (0)