Skip to content

Commit eec12f7

Browse files
committed
Schemas deduplication now compare generated schemas to reduce automatically named schemas (Entity / Entity2 / Entity3 / ...).
1 parent c39f3b9 commit eec12f7

14 files changed

+403
-235
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# CHANGELOG
22

3+
## 5.1.0
4+
* Schemas deduplication now compare generated schemas to reduce automatically named schemas (Entity / Entity2 / Entity3 / ...).
5+
36
## 4.38.0
47
* Added a `#[Ignore]` attribute that allows a property to be excluded from the generated schema.
58
```php

src/Model/ModelRegistry.php

+49-4
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ final class ModelRegistry
4848
*/
4949
private array $names = [];
5050

51+
/**
52+
* @var array<string, array{'schema': string, 'model': Model}[]> List of schemas and Model per type
53+
*/
54+
private array $schemasPerType = [];
55+
5156
/**
5257
* @var iterable<ModelDescriberInterface>
5358
*/
@@ -82,13 +87,36 @@ public function __construct($modelDescribers, OA\OpenApi $api, array $alternativ
8287
public function register(Model $model): string
8388
{
8489
$hash = $model->getHash();
85-
if (!isset($this->models[$hash])) {
86-
$this->models[$hash] = $model;
87-
$this->unregistered[] = $hash;
88-
}
90+
$type = $this->getTypeShortName($model->getType());
91+
92+
$schema = null;
8993
if (!isset($this->names[$hash])) {
9094
$this->names[$hash] = $this->generateModelName($model);
9195
$this->registeredModelNames[$this->names[$hash]] = $model;
96+
97+
$schema = $this->getSchemaWithoutRegistration($model);
98+
99+
// Only try to match schemas if we successfully got one
100+
if (null !== $schema) {
101+
foreach ($this->schemasPerType[$type] ?? [] as $schemaAndModel) {
102+
if ($schemaAndModel['schema'] === json_encode($schema->jsonSerialize())) {
103+
$newHash = $schemaAndModel['model']->getHash();
104+
unset($this->names[$hash], $this->registeredModelNames[$hash]);
105+
106+
return OA\Components::SCHEMA_REF.$this->names[$newHash];
107+
}
108+
}
109+
}
110+
}
111+
112+
if (!isset($this->models[$hash])) {
113+
$this->models[$hash] = $model;
114+
$this->unregistered[] = $hash;
115+
$this->unregistered = array_unique($this->unregistered);
116+
// Only store schema if it was successfully generated
117+
if (null !== $schema) {
118+
$this->schemasPerType[$type][] = ['schema' => json_encode($schema), 'model' => $model];
119+
}
92120
}
93121

94122
// Reserve the name
@@ -97,6 +125,23 @@ public function register(Model $model): string
97125
return OA\Components::SCHEMA_REF.$this->names[$hash];
98126
}
99127

128+
private function getSchemaWithoutRegistration(Model $model): ?OA\Schema
129+
{
130+
foreach ($this->modelDescribers as $modelDescriber) {
131+
if ($modelDescriber instanceof ModelRegistryAwareInterface) {
132+
$modelDescriber->setModelRegistry($this);
133+
}
134+
if ($modelDescriber->supports($model)) {
135+
$schema = new OA\Schema([]);
136+
$modelDescriber->describe($model, $schema);
137+
138+
return $schema;
139+
}
140+
}
141+
142+
return null;
143+
}
144+
100145
/**
101146
* @internal
102147
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the NelmioApiDocBundle package.
5+
*
6+
* (c) Nelmio
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+
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
13+
14+
use Nelmio\ApiDocBundle\Attribute\Model;
15+
use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article81WithGroups;
16+
use OpenApi\Attributes as OA;
17+
use Symfony\Component\Routing\Annotation\Route;
18+
19+
class ApiController81Collisions
20+
{
21+
#[Route('/article_81_group_full', methods: ['GET'])]
22+
#[OA\Response(response: 200, description: 'Success', content: new Model(type: Article81WithGroups::class, groups: ['full']))]
23+
public function article81GroupFull()
24+
{
25+
}
26+
27+
#[Route('/article_81_group_default', methods: ['GET'])]
28+
#[OA\Response(response: 200, description: 'Success', content: new Model(type: Article81WithGroups::class, groups: ['default']))]
29+
public function article81GroupDefault()
30+
{
31+
}
32+
33+
#[Route('/article_81_group_default_and_full', methods: ['GET'])]
34+
#[OA\Response(response: 200, description: 'Success', content: new Model(type: Article81WithGroups::class, groups: ['default', 'full']))]
35+
public function article81GroupDefaultAndFull()
36+
{
37+
}
38+
39+
#[Route('/article_81_group_empty', methods: ['GET'])]
40+
#[OA\Response(response: 200, description: 'Success', content: new Model(type: Article81WithGroups::class, groups: []))]
41+
public function article81GroupEmpty()
42+
{
43+
}
44+
}

tests/Functional/ControllerTest.php

+10
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,16 @@ public static function provideAttributeTestCases(): \Generator
157157
],
158158
];
159159

160+
yield 'Name collision with groups' => [
161+
[
162+
'name' => 'ApiController81Collisions',
163+
'type' => 'attribute',
164+
],
165+
null,
166+
[],
167+
[__DIR__.'/Configs/EnableSerializer.yaml']
168+
];
169+
160170
if (property_exists(MapRequestPayload::class, 'type')) {
161171
yield 'Symfony 7.1 MapRequestPayload array type' => [
162172
[

tests/Functional/CsrfProtectionFunctionalTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function testTokenPropertyExistsPerDefaultIfEnabledPerFrameworkConfig():
3939
'type' => 'object',
4040
'properties' => [
4141
'quz' => [
42-
'$ref' => '#/components/schemas/User',
42+
'$ref' => '#/components/schemas/User2',
4343
],
4444
'_token' => [
4545
'description' => 'CSRF token',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the NelmioApiDocBundle package.
5+
*
6+
* (c) Nelmio
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+
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
13+
14+
use Symfony\Component\Serializer\Attribute\Groups;
15+
16+
class Article81WithGroups
17+
{
18+
#[Groups(['default'])]
19+
public string $default;
20+
21+
#[Groups(['default', 'full'])]
22+
public DummyWithGroups $defaultAndFull;
23+
24+
public string $noGroups;
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the NelmioApiDocBundle package.
5+
*
6+
* (c) Nelmio
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+
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
13+
14+
use Symfony\Component\Serializer\Attribute\Groups;
15+
16+
class DummyWithGroups
17+
{
18+
#[Groups(['default', 'full'])]
19+
public string $defaultAndFull;
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
{
2+
"openapi": "3.0.0",
3+
"info": {
4+
"title": "",
5+
"version": "0.0.0"
6+
},
7+
"paths": {
8+
"/article_81_group_full": {
9+
"get": {
10+
"operationId": "get_nelmio_apidoc_tests_functional_apicontroller81collisions_article81groupfull",
11+
"responses": {
12+
"200": {
13+
"description": "Success",
14+
"content": {
15+
"application/json": {
16+
"schema": {
17+
"$ref": "#/components/schemas/Article81WithGroups"
18+
}
19+
}
20+
}
21+
}
22+
}
23+
}
24+
},
25+
"/article_81_group_default": {
26+
"get": {
27+
"operationId": "get_nelmio_apidoc_tests_functional_apicontroller81collisions_article81groupdefault",
28+
"responses": {
29+
"200": {
30+
"description": "Success",
31+
"content": {
32+
"application/json": {
33+
"schema": {
34+
"$ref": "#/components/schemas/Article81WithGroups2"
35+
}
36+
}
37+
}
38+
}
39+
}
40+
}
41+
},
42+
"/article_81_group_default_and_full": {
43+
"get": {
44+
"operationId": "get_nelmio_apidoc_tests_functional_apicontroller81collisions_article81groupdefaultandfull",
45+
"responses": {
46+
"200": {
47+
"description": "Success",
48+
"content": {
49+
"application/json": {
50+
"schema": {
51+
"$ref": "#/components/schemas/Article81WithGroups2"
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
},
59+
"/article_81_group_empty": {
60+
"get": {
61+
"operationId": "get_nelmio_apidoc_tests_functional_apicontroller81collisions_article81groupempty",
62+
"responses": {
63+
"200": {
64+
"description": "Success",
65+
"content": {
66+
"application/json": {
67+
"schema": {
68+
"$ref": "#/components/schemas/Article81WithGroups3"
69+
}
70+
}
71+
}
72+
}
73+
}
74+
}
75+
}
76+
},
77+
"components": {
78+
"schemas": {
79+
"DummyWithGroups": {
80+
"required": [
81+
"defaultAndFull"
82+
],
83+
"properties": {
84+
"defaultAndFull": {
85+
"type": "string"
86+
}
87+
},
88+
"type": "object"
89+
},
90+
"Article81WithGroups": {
91+
"required": [
92+
"defaultAndFull"
93+
],
94+
"properties": {
95+
"defaultAndFull": {
96+
"$ref": "#/components/schemas/DummyWithGroups"
97+
}
98+
},
99+
"type": "object"
100+
},
101+
"Article81WithGroups2": {
102+
"required": [
103+
"default",
104+
"defaultAndFull"
105+
],
106+
"properties": {
107+
"default": {
108+
"type": "string"
109+
},
110+
"defaultAndFull": {
111+
"$ref": "#/components/schemas/DummyWithGroups"
112+
}
113+
},
114+
"type": "object"
115+
},
116+
"Article81WithGroups3": {
117+
"type": "object"
118+
}
119+
}
120+
}
121+
}

tests/Functional/Fixtures/Controller2209.json

+25-25
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,31 @@
2828
},
2929
"components": {
3030
"schemas": {
31+
"ArticleType81": {
32+
"type": "string",
33+
"enum": [
34+
"draft",
35+
"final"
36+
]
37+
},
38+
"ArticleType81IntBacked": {
39+
"type": "integer",
40+
"enum": [
41+
0,
42+
1
43+
]
44+
},
45+
"ArticleType81NotBacked": {
46+
"required": [
47+
"name"
48+
],
49+
"properties": {
50+
"name": {
51+
"type": "string"
52+
}
53+
},
54+
"type": "object"
55+
},
3156
"Article81": {
3257
"required": [
3358
"id",
@@ -58,31 +83,6 @@
5883
}
5984
},
6085
"type": "object"
61-
},
62-
"ArticleType81": {
63-
"type": "string",
64-
"enum": [
65-
"draft",
66-
"final"
67-
]
68-
},
69-
"ArticleType81IntBacked": {
70-
"type": "integer",
71-
"enum": [
72-
0,
73-
1
74-
]
75-
},
76-
"ArticleType81NotBacked": {
77-
"required": [
78-
"name"
79-
],
80-
"properties": {
81-
"name": {
82-
"type": "string"
83-
}
84-
},
85-
"type": "object"
8686
}
8787
}
8888
}

0 commit comments

Comments
 (0)