Skip to content

Commit bc97ae4

Browse files
committed
Merge 3.2
2 parents ddc9742 + ce560cf commit bc97ae4

File tree

11 files changed

+158
-19
lines changed

11 files changed

+158
-19
lines changed

features/openapi/docs.feature

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,44 @@ Feature: Documentation support
393393
"$ref": "#\/components\/schemas\/WrappedResponseEntity-read"
394394
}
395395
"""
396+
397+
Scenario: Retrieve the OpenAPI documentation with 3.0 specification
398+
Given I send a "GET" request to "/docs.jsonopenapi?spec_version=3.0.0"
399+
Then the response status code should be 200
400+
And the response should be in JSON
401+
And the JSON node "openapi" should be equal to "3.0.0"
402+
And the JSON node "components.schemas.DummyBoolean" should be equal to:
403+
"""
404+
{
405+
"type": "object",
406+
"description": "",
407+
"deprecated": false,
408+
"properties": {
409+
"id": {
410+
"readOnly": true,
411+
"anyOf": [
412+
{
413+
"type": "integer"
414+
},
415+
{
416+
"type": "null"
417+
}
418+
]
419+
},
420+
"isDummyBoolean": {
421+
"anyOf": [
422+
{
423+
"type": "boolean"
424+
},
425+
{
426+
"type": "null"
427+
}
428+
]
429+
},
430+
"dummyBoolean": {
431+
"readOnly": true,
432+
"type": "boolean"
433+
}
434+
}
435+
}
436+
"""

phpstan.neon.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ parameters:
3333
- src/Serializer/CacheableSupportsMethodInterface.php
3434
- tests/Hal/Serializer/ItemNormalizerTest.php
3535
- tests/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php
36+
- src/Symfony/Bundle/ArgumentResolver/CompatibleValueResolverInterface.php
3637
earlyTerminatingMethodCalls:
3738
PHPUnit\Framework\Constraint\Constraint:
3839
- fail
@@ -98,3 +99,4 @@ parameters:
9899
# Backward compatibility
99100
- '#Call to method hasCacheableSupportsMethod\(\) on an unknown class Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface\.#'
100101
- '#Class Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface not found\.#'
102+
- '#Access to undefined constant Symfony\\Component\\HttpKernel\\HttpKernelInterface::MASTER_REQUEST\.#'

src/Documentation/Action/DocumentationAction.php

Lines changed: 18 additions & 2 deletions
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\LegacyOpenApiNormalizer;
2425
use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer;
2526
use ApiPlatform\State\ProcessorInterface;
2627
use ApiPlatform\State\ProviderInterface;
@@ -60,7 +61,11 @@ public function __invoke(Request $request = null)
6061
return new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version);
6162
}
6263

63-
$context = ['api_gateway' => $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY), 'base_url' => $request->getBaseUrl()];
64+
$context = [
65+
'api_gateway' => $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY),
66+
'base_url' => $request->getBaseUrl(),
67+
'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION),
68+
];
6469
$request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context);
6570
$format = $this->getRequestFormat($request, $this->documentationFormats);
6671

@@ -78,7 +83,18 @@ private function getOpenApiDocumentation(array $context, string $format, Request
7883
{
7984
if ($this->provider && $this->processor) {
8085
$context['request'] = $request;
81-
$operation = new Get(class: OpenApi::class, read: true, serialize: true, provider: fn () => $this->openApiFactory->__invoke($context), normalizationContext: [ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null], outputFormats: $this->documentationFormats);
86+
$operation = new Get(
87+
class: OpenApi::class,
88+
read: true,
89+
serialize: true,
90+
provider: fn () => $this->openApiFactory->__invoke($context),
91+
normalizationContext: [
92+
ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null,
93+
LegacyOpenApiNormalizer::SPEC_VERSION => $context['spec_version'] ?? null,
94+
],
95+
outputFormats: $this->documentationFormats
96+
);
97+
8298
if ('html' === $format) {
8399
$operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true);
84100
}

src/Documentation/Action/EntrypointAction.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Metadata\Get;
1818
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
1919
use ApiPlatform\Metadata\Resource\ResourceNameCollection;
20+
use ApiPlatform\OpenApi\Serializer\LegacyOpenApiNormalizer;
2021
use ApiPlatform\State\ProcessorInterface;
2122
use ApiPlatform\State\ProviderInterface;
2223
use Symfony\Component\HttpFoundation\Request;
@@ -41,7 +42,10 @@ public function __construct(
4142
public function __invoke(Request $request)
4243
{
4344
static::$resourceNameCollection = $this->resourceNameCollectionFactory->create();
44-
$context = ['request' => $request];
45+
$context = [
46+
'request' => $request,
47+
'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION),
48+
];
4549
$request->attributes->set('_api_platform_disable_listeners', true);
4650
$operation = new Get(outputFormats: $this->documentationFormats, read: true, serialize: true, class: Entrypoint::class, provider: [self::class, 'provide']);
4751
$request->attributes->set('_api_operation', $operation);

src/OpenApi/Command/OpenApiCommand.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
5353
{
5454
$filesystem = new Filesystem();
5555
$io = new SymfonyStyle($input, $output);
56-
$data = $this->normalizer->normalize($this->openApiFactory->__invoke(), 'json');
56+
$data = $this->normalizer->normalize($this->openApiFactory->__invoke(), 'json', [
57+
'spec_version' => $input->getOption('spec-version'),
58+
]);
5759
$content = $input->getOption('yaml')
5860
? Yaml::dump($data, 10, 2, Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)
5961
: (json_encode($data, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES) ?: '');
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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\OpenApi\Serializer;
15+
16+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
17+
18+
final class LegacyOpenApiNormalizer implements NormalizerInterface
19+
{
20+
public const SPEC_VERSION = 'spec_version';
21+
private array $defaultContext = [
22+
self::SPEC_VERSION => '3.1.0',
23+
];
24+
25+
public function __construct(private readonly NormalizerInterface $decorated, $defaultContext = [])
26+
{
27+
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
28+
}
29+
30+
public function normalize(mixed $object, string $format = null, array $context = []): array
31+
{
32+
$openapi = $this->decorated->normalize($object, $format, $context);
33+
34+
if ('3.0.0' !== ($context['spec_version'] ?? null)) {
35+
return $openapi;
36+
}
37+
38+
$schemas = &$openapi['components']['schemas'];
39+
$openapi['openapi'] = '3.0.0';
40+
foreach ($openapi['components']['schemas'] as $name => $component) {
41+
foreach ($component['properties'] ?? [] as $property => $value) {
42+
if (\is_array($value['type'] ?? false)) {
43+
foreach ($value['type'] as $type) {
44+
$schemas[$name]['properties'][$property]['anyOf'][] = ['type' => $type];
45+
}
46+
unset($schemas[$name]['properties'][$property]['type']);
47+
}
48+
unset($schemas[$name]['properties'][$property]['owl:maxCardinality']);
49+
}
50+
}
51+
52+
return $openapi;
53+
}
54+
55+
/**
56+
* {@inheritdoc}
57+
*/
58+
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
59+
{
60+
return $this->decorated->supportsNormalization($data, $format, $context);
61+
}
62+
63+
public function getSupportedTypes($format): array
64+
{
65+
return $this->decorated->getSupportedTypes($format);
66+
}
67+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@
5858

5959
<service id="api_platform.openapi.normalizer.api_gateway" class="ApiPlatform\OpenApi\Serializer\ApiGatewayNormalizer" public="false" decorates="api_platform.openapi.normalizer" decoration-priority="-1">
6060
<argument type="service" id="api_platform.openapi.normalizer.api_gateway.inner" />
61-
<tag name="serializer.normalizer" priority="-780" />
61+
<tag name="serializer.normalizer" />
62+
</service>
63+
64+
<service id="api_platform.openapi.normalizer.legacy" class="ApiPlatform\OpenApi\Serializer\LegacyOpenApiNormalizer" public="false" decorates="api_platform.openapi.normalizer.api_gateway" decoration-priority="-2">
65+
<argument type="service" id="api_platform.openapi.normalizer.legacy.inner" />
66+
<tag name="serializer.normalizer" />
6267
</service>
6368
<service id="ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface" alias="api_platform.openapi.factory" />
6469

tests/Documentation/Action/DocumentationActionTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ public function testDocumentationAction(): void
5252
$requestProphecy->headers = $this->prophesize(ParameterBagInterface::class)->reveal();
5353
$requestProphecy->getBaseUrl()->willReturn('/api')->shouldBeCalledTimes(1);
5454
$queryProphecy->getBoolean('api_gateway')->willReturn(true)->shouldBeCalledTimes(1);
55+
$queryProphecy->get('spec_version')->willReturn('3.1.0')->shouldBeCalledTimes(1);
5556
$attributesProphecy->get('_api_normalization_context', [])->willReturn(['foo' => 'bar'])->shouldBeCalledTimes(1);
5657
$attributesProphecy->get('_format')->willReturn(null)->shouldBeCalledTimes(1);
57-
$attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true])->shouldBeCalledTimes(1);
58+
$attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => '3.1.0'])->shouldBeCalledTimes(1);
5859

5960
$documentation = new DocumentationAction($this->prophesize(ResourceNameCollectionFactoryInterface::class)->reveal(), 'my api', '', '1.0.0', $openApiFactoryProphecy->reveal());
6061
$this->assertInstanceOf(OpenApi::class, $documentation($requestProphecy->reveal()));
@@ -75,8 +76,9 @@ public function testDocumentationActionWithoutOpenApiFactory(): void
7576
$requestProphecy->query = $queryProphecy->reveal();
7677
$requestProphecy->getBaseUrl()->willReturn('/api')->shouldBeCalledTimes(1);
7778
$queryProphecy->getBoolean('api_gateway')->willReturn(true)->shouldBeCalledTimes(1);
79+
$queryProphecy->get('spec_version')->willReturn('3.1.0')->shouldBeCalledTimes(1);
7880
$attributesProphecy->get('_api_normalization_context', [])->willReturn(['foo' => 'bar'])->shouldBeCalledTimes(1);
79-
$attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true])->shouldBeCalledTimes(1);
81+
$attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => '3.1.0'])->shouldBeCalledTimes(1);
8082
$resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
8183
$resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection(['dummies']))->shouldBeCalled();
8284

tests/Symfony/EventListener/AddTagsListenerTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ public function testAddTagsWithXKey(): void
209209
$event = new ResponseEvent(
210210
$this->prophesize(HttpKernelInterface::class)->reveal(),
211211
new Request([], [], ['_resources' => ['/foo' => '/foo', '/bar' => '/bar'], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']),
212-
HttpKernelInterface::MASTER_REQUEST,
212+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
213213
$response
214214
);
215215

@@ -239,7 +239,7 @@ public function testAddTagsWithoutHeader(): void
239239
$event = new ResponseEvent(
240240
$this->prophesize(HttpKernelInterface::class)->reveal(),
241241
new Request([], [], ['_resources' => ['/foo' => '/foo', '/bar' => '/bar'], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']),
242-
HttpKernelInterface::MASTER_REQUEST,
242+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
243243
$response
244244
);
245245

@@ -269,7 +269,7 @@ public function testDummyHeaderTag(): void
269269
$event = new ResponseEvent(
270270
$this->prophesize(HttpKernelInterface::class)->reveal(),
271271
new Request([], [], ['_resources' => ['/foo' => '/foo', '/bar' => '/bar'], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']),
272-
HttpKernelInterface::MASTER_REQUEST,
272+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
273273
$response
274274
);
275275

tests/Symfony/EventListener/WriteListenerTest.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public function testOnKernelViewWithControllerResultAndPersist(): void
8383
$event = new ViewEvent(
8484
$this->prophesize(HttpKernelInterface::class)->reveal(),
8585
$request,
86-
HttpKernelInterface::MASTER_REQUEST,
86+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
8787
$operationResource
8888
);
8989

@@ -118,7 +118,7 @@ public function testOnKernelViewDoNotCallIriConverterWhenOutputClassDisabled():
118118
$event = new ViewEvent(
119119
$this->prophesize(HttpKernelInterface::class)->reveal(),
120120
$request,
121-
HttpKernelInterface::MASTER_REQUEST,
121+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
122122
$operationResource
123123
);
124124

@@ -144,7 +144,7 @@ public function testOnKernelViewWithControllerResultAndRemove(): void
144144
$event = new ViewEvent(
145145
$this->prophesize(HttpKernelInterface::class)->reveal(),
146146
$request,
147-
HttpKernelInterface::MASTER_REQUEST,
147+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
148148
$operationResource
149149
);
150150

@@ -172,7 +172,7 @@ public function testOnKernelViewWithSafeMethod(): void
172172
$event = new ViewEvent(
173173
$this->prophesize(HttpKernelInterface::class)->reveal(),
174174
$request,
175-
HttpKernelInterface::MASTER_REQUEST,
175+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
176176
$operationResource
177177
);
178178

@@ -190,7 +190,7 @@ public function testDoNotWriteWhenControllerResultIsResponse(): void
190190
$event = new ViewEvent(
191191
$this->prophesize(HttpKernelInterface::class)->reveal(),
192192
$request,
193-
HttpKernelInterface::MASTER_REQUEST,
193+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
194194
$response
195195
);
196196

@@ -215,7 +215,7 @@ public function testDoNotWriteWhenCant(): void
215215
$event = new ViewEvent(
216216
$this->prophesize(HttpKernelInterface::class)->reveal(),
217217
$request,
218-
HttpKernelInterface::MASTER_REQUEST,
218+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
219219
$operationResource
220220
);
221221

@@ -237,7 +237,7 @@ public function testOnKernelViewWithNoResourceClass(): void
237237
$event = new ViewEvent(
238238
$this->prophesize(HttpKernelInterface::class)->reveal(),
239239
$request,
240-
HttpKernelInterface::MASTER_REQUEST,
240+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
241241
$operationResource
242242
);
243243

@@ -268,7 +268,7 @@ public function testOnKernelViewInvalidIdentifiers(): void
268268
$event = new ViewEvent(
269269
$this->prophesize(HttpKernelInterface::class)->reveal(),
270270
$request,
271-
HttpKernelInterface::MASTER_REQUEST,
271+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
272272
$attributeResource
273273
);
274274

tests/Symfony/Validator/Metadata/ValidationExceptionListenerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public function getConstraintViolationList(): ConstraintViolationListInterface
103103
$exceptionEvent = new ExceptionEvent(
104104
$this->prophesize(HttpKernelInterface::class)->reveal(),
105105
new Request(),
106-
HttpKernelInterface::MASTER_REQUEST,
106+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
107107
$exception
108108
);
109109

0 commit comments

Comments
 (0)