From 382d1f2c92ff97c58707ed0452413ea7b7d4a733 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 3 Jun 2026 15:58:43 +0200 Subject: [PATCH 1/3] fix(openapi): reject non-Parameter values in OpenApiFactory::hasParameter Operation::parameters is documented as ?Parameter[] (OpenApi\Model\Parameter) but accepted any array element at runtime. Passing a Metadata\QueryParameter (or any Metadata\Parameter) inside the user-supplied openapi Operation crashed with an opaque "undefined method getName()" deep in OpenApiFactory. Guard hasParameter() with an instanceof Parameter check and throw an ApiPlatform\Metadata\Exception\InvalidArgumentException naming the offending class and pointing at the resource-level "parameters" option for Metadata\Parameter declarations. Fixes #8182 --- src/OpenApi/Factory/OpenApiFactory.php | 5 ++ .../Tests/Factory/OpenApiFactoryTest.php | 59 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index cc9fc432eb..471ce72756 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -20,6 +20,7 @@ use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Error; use ApiPlatform\Metadata\ErrorResource; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Exception\OperationNotFoundException; use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException; @@ -946,6 +947,10 @@ private function appendSchemaDefinitions(\ArrayObject $schemas, \ArrayObject $de private function hasParameter(Operation $operation, Parameter $parameter): ?array { foreach ($operation->getParameters() as $key => $existingParameter) { + if (!$existingParameter instanceof Parameter) { + throw new InvalidArgumentException(\sprintf('OpenAPI operation parameters must be instances of "%s", "%s" given. Use the resource-level "parameters" option to declare "%s" instances.', Parameter::class, get_debug_type($existingParameter), \ApiPlatform\Metadata\Parameter::class)); + } + if ($existingParameter->getName() === $parameter->getName() && $existingParameter->getIn() === $parameter->getIn()) { return [$key, $existingParameter]; } diff --git a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php index c956e10808..68b72e5c45 100644 --- a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php +++ b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php @@ -1438,4 +1438,63 @@ public function testGetExtensionPropertiesWithFalseValue(): void $openApi = $factory->__invoke(); } + + public function testMetadataParameterInOpenApiOperationParametersThrows(): void + { + $resourceNameCollectionFactory = $this->createMock(ResourceNameCollectionFactoryInterface::class); + $resourceCollectionMetadataFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $propertyNameCollectionFactory = $this->createMock(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class); + $definitionNameFactory = new DefinitionNameFactory([]); + + $resourceCollectionMetadata = new ResourceMetadataCollection(Dummy::class, [(new ApiResource(operations: [ + (new GetCollection()) + ->withClass(Dummy::class) + ->withShortName('Dummy') + ->withName('api_dummies_get_collection') + ->withUriTemplate('/dummies') + ->withOpenapi(new Operation(parameters: [new \ApiPlatform\Metadata\QueryParameter(key: 'bar')])), + ]))->withClass(Dummy::class)]); + + $resourceCollectionMetadataFactory + ->method('create') + ->willReturnCallback(static fn (string $resourceClass): ResourceMetadataCollection => match ($resourceClass) { + default => new ResourceMetadataCollection($resourceClass, []), + Dummy::class => $resourceCollectionMetadata, + }); + + $resourceNameCollectionFactory->expects($this->once()) + ->method('create') + ->willReturn(new ResourceNameCollection([Dummy::class])); + + $propertyNameCollectionFactory->method('create')->willReturn(new PropertyNameCollection([])); + + $schemaFactory = new SchemaFactory( + resourceMetadataFactory: $resourceCollectionMetadataFactory, + propertyNameCollectionFactory: $propertyNameCollectionFactory, + propertyMetadataFactory: $propertyMetadataFactory, + nameConverter: new CamelCaseToSnakeCaseNameConverter(), + definitionNameFactory: $definitionNameFactory, + ); + + $factory = new OpenApiFactory( + $resourceNameCollectionFactory, + $resourceCollectionMetadataFactory, + $propertyNameCollectionFactory, + $propertyMetadataFactory, + $schemaFactory, + null, + [], + new Options('Test API', 'This is a test API.', '1.2.3'), + new PaginationOptions(), + null, + ['json' => ['application/problem+json']] + ); + + $this->expectException(\ApiPlatform\Metadata\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage(Parameter::class); + $this->expectExceptionMessage(\ApiPlatform\Metadata\QueryParameter::class); + + $factory->__invoke(); + } } From ae4f970462a66cfe28c6c4b60e14dffafad3a7c6 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 3 Jun 2026 16:20:52 +0200 Subject: [PATCH 2/3] fix(openapi): trim Parameter rejection message Drop the resource-level "parameters" hint per PR feedback; keep the expected/got class names which already point at the misuse. --- src/OpenApi/Factory/OpenApiFactory.php | 2 +- src/OpenApi/Tests/Factory/OpenApiFactoryTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 471ce72756..6273ea8ba5 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -948,7 +948,7 @@ private function hasParameter(Operation $operation, Parameter $parameter): ?arra { foreach ($operation->getParameters() as $key => $existingParameter) { if (!$existingParameter instanceof Parameter) { - throw new InvalidArgumentException(\sprintf('OpenAPI operation parameters must be instances of "%s", "%s" given. Use the resource-level "parameters" option to declare "%s" instances.', Parameter::class, get_debug_type($existingParameter), \ApiPlatform\Metadata\Parameter::class)); + throw new InvalidArgumentException(\sprintf('OpenAPI operation parameters must be instances of "%s", "%s" given.', Parameter::class, get_debug_type($existingParameter))); } if ($existingParameter->getName() === $parameter->getName() && $existingParameter->getIn() === $parameter->getIn()) { diff --git a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php index 68b72e5c45..2f025d3f27 100644 --- a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php +++ b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php @@ -1493,7 +1493,6 @@ public function testMetadataParameterInOpenApiOperationParametersThrows(): void $this->expectException(\ApiPlatform\Metadata\Exception\InvalidArgumentException::class); $this->expectExceptionMessage(Parameter::class); - $this->expectExceptionMessage(\ApiPlatform\Metadata\QueryParameter::class); $factory->__invoke(); } From 2cc8793f687b5cbd5a440f8218711bd3d0393440 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 3 Jun 2026 16:30:15 +0200 Subject: [PATCH 3/3] test(openapi): import namespaces instead of FQDN in parameter test --- src/OpenApi/Tests/Factory/OpenApiFactoryTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php index 2f025d3f27..89463d4fc6 100644 --- a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php +++ b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php @@ -21,6 +21,7 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Error as ErrorOperation; use ApiPlatform\Metadata\ErrorResource; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\HeaderParameter; @@ -33,6 +34,7 @@ use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; use ApiPlatform\Metadata\Put; +use ApiPlatform\Metadata\QueryParameter; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -1453,7 +1455,7 @@ public function testMetadataParameterInOpenApiOperationParametersThrows(): void ->withShortName('Dummy') ->withName('api_dummies_get_collection') ->withUriTemplate('/dummies') - ->withOpenapi(new Operation(parameters: [new \ApiPlatform\Metadata\QueryParameter(key: 'bar')])), + ->withOpenapi(new Operation(parameters: [new QueryParameter(key: 'bar')])), ]))->withClass(Dummy::class)]); $resourceCollectionMetadataFactory @@ -1491,7 +1493,7 @@ public function testMetadataParameterInOpenApiOperationParametersThrows(): void ['json' => ['application/problem+json']] ); - $this->expectException(\ApiPlatform\Metadata\Exception\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage(Parameter::class); $factory->__invoke();