Skip to content

Commit 32c5998

Browse files
authored
OpenAPI: Make sure we do not override defined parameters #4137 (#4138)
1 parent e74bf8e commit 32c5998

File tree

7 files changed

+96
-27
lines changed

7 files changed

+96
-27
lines changed

src/JsonSchema/SchemaFactory.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public function buildSchema(string $className, string $format = 'json', string $
8080
}
8181

8282
$version = $schema->getVersion();
83-
$definitionName = $this->buildDefinitionName($className, $format, $type, $operationType, $operationName, $serializerContext);
83+
$definitionName = $this->buildDefinitionName($className, $format, $inputOrOutputClass, $resourceMetadata, $serializerContext);
8484

8585
if (null === $operationType || null === $operationName) {
8686
$method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET';
@@ -239,10 +239,8 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
239239
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema;
240240
}
241241

242-
private function buildDefinitionName(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?array $serializerContext = null): string
242+
private function buildDefinitionName(string $className, string $format = 'json', ?string $inputOrOutputClass = null, ?ResourceMetadata $resourceMetadata = null, ?array $serializerContext = null): string
243243
{
244-
[$resourceMetadata, $serializerContext,, $inputOrOutputClass] = $this->getMetadata($className, $type, $operationType, $operationName, $serializerContext);
245-
246244
$prefix = $resourceMetadata ? $resourceMetadata->getShortName() : (new \ReflectionClass($className))->getShortName();
247245
if (null !== $inputOrOutputClass && $className !== $inputOrOutputClass) {
248246
$parts = explode('\\', $inputOrOutputClass);

src/JsonSchema/TypeFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ private function getClassType(?string $className, string $format, ?bool $readabl
142142
throw new \LogicException('The schema factory must be injected by calling the "setSchemaFactory" method.');
143143
}
144144

145-
$subSchema = $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, null, $subSchema, $serializerContext);
145+
$subSchema = $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, null, $subSchema, $serializerContext, false);
146146

147147
return ['$ref' => $subSchema['$ref']];
148148
}

src/OpenApi/Factory/OpenApiFactory.php

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,19 @@ public function __invoke(array $context = []): OpenApi
8686
$servers = '/' === $baseUrl || '' === $baseUrl ? [new Model\Server('/')] : [new Model\Server($baseUrl)];
8787
$paths = new Model\Paths();
8888
$links = [];
89-
$schemas = [];
89+
$schemas = new \ArrayObject();
9090

9191
foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
9292
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
9393
$resourceShortName = $resourceMetadata->getShortName();
9494

9595
// Items needs to be parsed first to be able to reference the lines from the collection operation
9696
[$itemOperationLinks, $itemOperationSchemas] = $this->collectPaths($resourceMetadata, $resourceClass, OperationType::ITEM, $context, $paths, $links, $schemas);
97-
$schemas += $itemOperationSchemas;
97+
$this->appendSchemaDefinitions($schemas, $itemOperationSchemas);
9898
[$collectionOperationLinks, $collectionOperationSchemas] = $this->collectPaths($resourceMetadata, $resourceClass, OperationType::COLLECTION, $context, $paths, $links, $schemas);
9999

100100
[$subresourceOperationLinks, $subresourceOperationSchemas] = $this->collectPaths($resourceMetadata, $resourceClass, OperationType::SUBRESOURCE, $context, $paths, $links, $schemas);
101-
$schemas += $collectionOperationSchemas;
101+
$this->appendSchemaDefinitions($schemas, $collectionOperationSchemas);
102102
}
103103

104104
$securitySchemes = $this->getSecuritySchemes();
@@ -113,7 +113,7 @@ public function __invoke(array $context = []): OpenApi
113113
$servers,
114114
$paths,
115115
new Model\Components(
116-
new \ArrayObject($schemas),
116+
$schemas,
117117
new \ArrayObject(),
118118
new \ArrayObject(),
119119
new \ArrayObject(),
@@ -128,7 +128,7 @@ public function __invoke(array $context = []): OpenApi
128128
/**
129129
* @return array | array
130130
*/
131-
private function collectPaths(ResourceMetadata $resourceMetadata, string $resourceClass, string $operationType, array $context, Model\Paths $paths, array &$links, array $schemas = []): array
131+
private function collectPaths(ResourceMetadata $resourceMetadata, string $resourceClass, string $operationType, array $context, Model\Paths $paths, array &$links, \ArrayObject $schemas): array
132132
{
133133
$resourceShortName = $resourceMetadata->getShortName();
134134
$operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : (OperationType::ITEM === $operationType ? $resourceMetadata->getItemOperations() : $this->subresourceOperationFactory->create($resourceClass));
@@ -155,12 +155,14 @@ private function collectPaths(ResourceMetadata $resourceMetadata, string $resour
155155
$linkedOperationId = 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM);
156156
$pathItem = $paths->getPath($path) ?: new Model\PathItem();
157157
$forceSchemaCollection = OperationType::SUBRESOURCE === $operationType ? ($operation['collection'] ?? false) : false;
158+
$schema = new Schema('openapi');
159+
$schema->setDefinitions($schemas);
158160

159161
$operationOutputSchemas = [];
160162
foreach ($responseMimeTypes as $operationFormat) {
161-
$operationOutputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_OUTPUT, $operationType, $operationName, new Schema('openapi'), null, $forceSchemaCollection);
162-
$schemas += $operationOutputSchema->getDefinitions()->getArrayCopy();
163+
$operationOutputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_OUTPUT, $operationType, $operationName, $schema, null, $forceSchemaCollection);
163164
$operationOutputSchemas[$operationFormat] = $operationOutputSchema;
165+
$this->appendSchemaDefinitions($schemas, $operationOutputSchema->getDefinitions());
164166
}
165167

166168
$parameters = [];
@@ -175,19 +177,42 @@ private function collectPaths(ResourceMetadata $resourceMetadata, string $resour
175177
// Set up parameters
176178
if (OperationType::ITEM === $operationType) {
177179
foreach ($identifiers as $parameterName => $identifier) {
178-
$parameters[] = new Model\Parameter(\is_string($parameterName) ? $parameterName : $identifier, 'path', 'Resource identifier', true, false, false, ['type' => 'string']);
180+
$parameterName = \is_string($parameterName) ? $parameterName : $identifier;
181+
$parameter = new Model\Parameter($parameterName, 'path', 'Resource identifier', true, false, false, ['type' => 'string']);
182+
if ($this->hasParameter($parameter, $parameters)) {
183+
continue;
184+
}
185+
186+
$parameters[] = $parameter;
179187
}
180188
$links[$operationId] = $this->getLink($resourceClass, $operationId, $path);
181189
} elseif (OperationType::COLLECTION === $operationType && 'GET' === $method) {
182-
$parameters = array_merge($parameters, $this->getPaginationParameters($resourceMetadata, $operationName), $this->getFiltersParameters($resourceMetadata, $operationName, $resourceClass));
190+
foreach (array_merge($this->getPaginationParameters($resourceMetadata, $operationName), $this->getFiltersParameters($resourceMetadata, $operationName, $resourceClass)) as $parameter) {
191+
if ($this->hasParameter($parameter, $parameters)) {
192+
continue;
193+
}
194+
195+
$parameters[] = $parameter;
196+
}
183197
} elseif (OperationType::SUBRESOURCE === $operationType) {
184198
foreach ($operation['identifiers'] as $parameterName => [$class, $property]) {
185-
$parameters[] = new Model\Parameter($parameterName, 'path', $this->resourceMetadataFactory->create($class)->getShortName().' identifier', true, false, false, ['type' => 'string']);
199+
$parameter = new Model\Parameter($parameterName, 'path', $this->resourceMetadataFactory->create($class)->getShortName().' identifier', true, false, false, ['type' => 'string']);
200+
if ($this->hasParameter($parameter, $parameters)) {
201+
continue;
202+
}
203+
204+
$parameters[] = $parameter;
186205
}
187206

188207
if ($operation['collection']) {
189208
$subresourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
190-
$parameters = array_merge($parameters, $this->getPaginationParameters($resourceMetadata, $operationName), $this->getFiltersParameters($subresourceMetadata, $operationName, $resourceClass));
209+
foreach (array_merge($this->getPaginationParameters($resourceMetadata, $operationName), $this->getFiltersParameters($subresourceMetadata, $operationName, $resourceClass)) as $parameter) {
210+
if ($this->hasParameter($parameter, $parameters)) {
211+
continue;
212+
}
213+
214+
$parameters[] = $parameter;
215+
}
191216
}
192217
}
193218

@@ -241,9 +266,9 @@ private function collectPaths(ResourceMetadata $resourceMetadata, string $resour
241266
} elseif ('PUT' === $method || 'POST' === $method || 'PATCH' === $method) {
242267
$operationInputSchemas = [];
243268
foreach ($requestMimeTypes as $operationFormat) {
244-
$operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, $operationType, $operationName, new Schema('openapi'), null, $forceSchemaCollection);
245-
$schemas += $operationInputSchema->getDefinitions()->getArrayCopy();
269+
$operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, $operationType, $operationName, $schema, null, $forceSchemaCollection);
246270
$operationInputSchemas[$operationFormat] = $operationInputSchema;
271+
$this->appendSchemaDefinitions($schemas, $operationInputSchema->getDefinitions());
247272
}
248273

249274
$requestBody = new Model\RequestBody(sprintf('The %s %s resource', 'POST' === $method ? 'new' : 'updated', $resourceShortName), $this->buildContent($requestMimeTypes, $operationInputSchemas), true);
@@ -396,7 +421,7 @@ private function getFiltersParameters(ResourceMetadata $resourceMetadata, string
396421
$schema,
397422
'array' === $schema['type'] && \in_array($data['type'],
398423
[Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT], true) ? 'deepObject' : 'form',
399-
'array' === $schema['type'],
424+
$data['openapi']['explode'] ?? ('array' === $schema['type']),
400425
$data['openapi']['allowReserved'] ?? false,
401426
$data['openapi']['example'] ?? null,
402427
isset($data['openapi']['examples']
@@ -486,4 +511,25 @@ private function getSecuritySchemes(): array
486511

487512
return $securitySchemes;
488513
}
514+
515+
private function appendSchemaDefinitions(\ArrayObject $schemas, \ArrayObject $definitions): void
516+
{
517+
foreach ($definitions as $key => $value) {
518+
$schemas[$key] = $value;
519+
}
520+
}
521+
522+
/**
523+
* @param Model\Parameter[] $parameters
524+
*/
525+
private function hasParameter(Model\Parameter $parameter, array $parameters): bool
526+
{
527+
foreach ($parameters as $existingParameter) {
528+
if ($existingParameter->getName() === $parameter->getName() && $existingParameter->getIn() === $parameter->getIn()) {
529+
return true;
530+
}
531+
}
532+
533+
return false;
534+
}
489535
}

src/OpenApi/Model/Encoding.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,21 @@ public function canExplode(): bool
5252
return $this->explode;
5353
}
5454

55+
public function getExplode(): bool
56+
{
57+
return $this->explode;
58+
}
59+
5560
public function canAllowReserved(): bool
5661
{
5762
return $this->allowReserved;
5863
}
5964

65+
public function getAllowReserved(): bool
66+
{
67+
return $this->allowReserved;
68+
}
69+
6070
public function withContentType(string $contentType): self
6171
{
6272
$clone = clone $this;

src/OpenApi/Model/Parameter.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public function getDeprecated(): bool
8383
return $this->deprecated;
8484
}
8585

86-
public function canAllowEmptyValue(): bool
86+
public function getAllowEmptyValue(): bool
8787
{
8888
return $this->allowEmptyValue;
8989
}
@@ -103,11 +103,21 @@ public function canExplode(): bool
103103
return $this->explode;
104104
}
105105

106+
public function getExplode(): bool
107+
{
108+
return $this->explode;
109+
}
110+
106111
public function canAllowReserved(): bool
107112
{
108113
return $this->allowReserved;
109114
}
110115

116+
public function getAllowReserved(): bool
117+
{
118+
return $this->allowReserved;
119+
}
120+
111121
public function getExample()
112122
{
113123
return $this->example;

tests/JsonSchema/TypeFactoryTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ public function testGetClassType(): void
372372
{
373373
$schemaFactoryProphecy = $this->prophesize(SchemaFactoryInterface::class);
374374

375-
$schemaFactoryProphecy->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, null, null, Argument::type(Schema::class), ['foo' => 'bar'])->will(function (array $args) {
375+
$schemaFactoryProphecy->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, null, null, Argument::type(Schema::class), ['foo' => 'bar'], false)->will(function (array $args) {
376376
$args[5]['$ref'] = 'ref';
377377

378378
return $args[5];

tests/OpenApi/Factory/OpenApiFactoryTest.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ public function testInvoke(): void
7777
'custom' => ['method' => 'HEAD', 'path' => '/foo/{id}', 'openapi_context' => [
7878
'description' => 'Custom description',
7979
'parameters' => [
80-
['description' => 'Test parameter', 'name' => 'param', 'in' => 'path', 'type' => 'string', 'required' => true, 'default' => 'BOTH'],
80+
['description' => 'Test parameter', 'name' => 'param', 'in' => 'path', 'required' => true],
81+
['description' => 'Replace parameter', 'name' => 'id', 'in' => 'path', 'required' => true, 'schema' => ['type' => 'string', 'format' => 'uuid']],
8182
],
8283
'tags' => ['Dummy', 'Profile'],
8384
'responses' => [
@@ -117,7 +118,11 @@ public function testInvoke(): void
117118
'formats' => ['method' => 'PUT', 'path' => '/formatted/{id}', 'output_formats' => ['json' => ['application/json'], 'csv' => ['text/csv']], 'input_formats' => ['json' => ['application/json'], 'csv' => ['text/csv']]],
118119
],
119120
[
120-
'get' => ['method' => 'GET'] + self::OPERATION_FORMATS,
121+
'get' => ['method' => 'GET', 'openapi_context' => [
122+
'parameters' => [
123+
['description' => 'Test modified collection page number', 'name' => 'page', 'in' => 'query', 'required' => false, 'schema' => ['type' => 'integer', 'default' => 1], 'allowEmptyValue' => true],
124+
],
125+
]] + self::OPERATION_FORMATS,
121126
'post' => ['method' => 'POST'] + self::OPERATION_FORMATS,
122127
'filtered' => ['method' => 'GET', 'filters' => ['f1', 'f2', 'f3', 'f4', 'f5'], 'path' => '/filtered'] + self::OPERATION_FORMATS,
123128
'paginated' => ['method' => 'GET', 'pagination_client_enabled' => true, 'pagination_client_items_per_page' => true, 'pagination_items_per_page' => 20, 'pagination_maximum_items_per_page' => 80, 'path' => '/paginated'] + self::OPERATION_FORMATS,
@@ -153,7 +158,7 @@ public function testInvoke(): void
153158
'type' => 'string',
154159
'required' => true,
155160
'strategy' => 'exact',
156-
'openapi' => ['example' => 'bar', 'deprecated' => true, 'allowEmptyValue' => true, 'allowReserved' => true],
161+
'openapi' => ['example' => 'bar', 'deprecated' => true, 'allowEmptyValue' => true, 'allowReserved' => true, 'explode' => true],
157162
]]),
158163
'f2' => new DummyFilter(['ha' => [
159164
'property' => 'foo',
@@ -306,7 +311,7 @@ public function testInvoke(): void
306311
'Retrieves the collection of Dummy resources.',
307312
null,
308313
[
309-
new Model\Parameter('page', 'query', 'The collection page number', false, false, true, [
314+
new Model\Parameter('page', 'query', 'Test modified collection page number', false, false, true, [
310315
'type' => 'integer',
311316
'default' => 1,
312317
]),
@@ -434,7 +439,7 @@ public function testInvoke(): void
434439
'Dummy',
435440
'Custom description',
436441
null,
437-
[new Model\Parameter('param', 'path', 'Test parameter', true), new Model\Parameter('id', 'path', 'Resource identifier', true, false, false, ['type' => 'string'])],
442+
[new Model\Parameter('param', 'path', 'Test parameter', true), new Model\Parameter('id', 'path', 'Replace parameter', true, false, false, ['type' => 'string', 'format' => 'uuid'])],
438443
new Model\RequestBody('Custom request body', new \ArrayObject([
439444
'multipart/form-data' => [
440445
'schema' => [
@@ -512,7 +517,7 @@ public function testInvoke(): void
512517
]),
513518
new Model\Parameter('name', 'query', '', true, true, true, [
514519
'type' => 'string',
515-
], 'form', false, true, 'bar'),
520+
], 'form', true, true, 'bar'),
516521
new Model\Parameter('ha', 'query', '', false, false, true, [
517522
'type' => 'integer',
518523
]),

0 commit comments

Comments
 (0)