From f9a7d12b03eb79cf2d925aff23616c9c11ba77e6 Mon Sep 17 00:00:00 2001 From: Maxcastel Date: Thu, 1 Oct 2026 15:33:16 +0200 Subject: [PATCH] fix(metadata): allow GraphQL operations in parameter attributes on properties --- ...meterResourceMetadataCollectionFactory.php | 4 + ...rResourceMetadataCollectionFactoryTest.php | 91 +++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php index 8d8b344527..ca22c024f3 100644 --- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php @@ -576,6 +576,10 @@ private function getDeclaredOperationClasses(ResourceMetadataCollection $resourc foreach ($resource->getOperations() ?? [] as $operation) { $operationClasses[] = $operation::class; } + + foreach ($resource->getGraphQlOperations() ?? [] as $operation) { + $operationClasses[] = $operation::class; + } } return $operationClasses; diff --git a/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php index e38e5d1a9c..06f97488dd 100644 --- a/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php @@ -20,6 +20,8 @@ use ApiPlatform\Metadata\FilterInterface; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\HeaderParameter; use ApiPlatform\Metadata\ParameterProviderFilterInterface; use ApiPlatform\Metadata\Parameters; @@ -884,6 +886,67 @@ public function testHeaderParameterOnPropertiesWithOperations(): void $this->assertTrue($getParameters->has('X-API-Key', HeaderParameter::class)); } + public function testQueryParameterOnPropertiesWithGraphQlOperations(): void + { + $nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class); + $nameCollection->method('create')->willReturn(new PropertyNameCollection(['name'])); + + $propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class); + $propertyMetadata->method('create')->willReturn( + new ApiProperty(readable: true), + ); + + $filterLocator = $this->createStub(ContainerInterface::class); + $filterLocator->method('has')->willReturn(false); + + $parameterFactory = new ParameterResourceMetadataCollectionFactory( + $nameCollection, + $propertyMetadata, + new AttributesResourceMetadataCollectionFactory(graphQlEnabled: true), + $filterLocator + ); + + $resource = $parameterFactory->create(QueryParameterOnPropertiesWithGraphQlOperations::class)[0]; + + $operations = array_values(iterator_to_array($resource->getOperations())); + $this->assertCount(1, $operations); + $this->assertInstanceOf(GetCollection::class, $operations[0]); + $this->assertFalse($operations[0]->getParameters()?->has('search', QueryParameter::class) ?? false); + + $graphQlCollectionOperations = array_values(array_filter( + $resource->getGraphQlOperations(), + static fn ($graphQlOperation): bool => $graphQlOperation instanceof QueryCollection, + )); + $this->assertCount(1, $graphQlCollectionOperations); + $this->assertTrue($graphQlCollectionOperations[0]->getParameters()->has('search', QueryParameter::class)); + } + + public function testQueryParameterOnPropertiesThrowsExceptionWhenGraphQlOperationIsNotDeclared(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(\sprintf('Parameter attribute on property "name" is restricted to the operation "%s" which is not declared on the resource "%s".', Mutation::class, QueryParameterOnPropertiesWithUndeclaredGraphQlOperation::class)); + + $nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class); + $nameCollection->method('create')->willReturn(new PropertyNameCollection(['name'])); + + $propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class); + $propertyMetadata->method('create')->willReturn( + new ApiProperty(readable: true), + ); + + $filterLocator = $this->createStub(ContainerInterface::class); + $filterLocator->method('has')->willReturn(false); + + $parameterFactory = new ParameterResourceMetadataCollectionFactory( + $nameCollection, + $propertyMetadata, + new AttributesResourceMetadataCollectionFactory(graphQlEnabled: true), + $filterLocator + ); + + $parameterFactory->create(QueryParameterOnPropertiesWithUndeclaredGraphQlOperation::class); + } + public function testNestedPropertyWithNameConverter(): void { $nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class); @@ -1400,3 +1463,31 @@ class HeaderParameterOnPropertiesWithOperations #[HeaderParameter(key: 'X-API-Key', description: 'API key header', operations: [new GetCollection(), new Get()])] public string $apiKey = ''; } + +#[ApiResource( + operations: [ + new GetCollection(), + ], + graphQlOperations: [ + new QueryCollection(), + ] +)] +class QueryParameterOnPropertiesWithGraphQlOperations +{ + #[QueryParameter(key: 'search', description: 'Search by name', operations: [new QueryCollection()])] + public string $name = ''; +} + +#[ApiResource( + operations: [ + new GetCollection(), + ], + graphQlOperations: [ + new QueryCollection(), + ] +)] +class QueryParameterOnPropertiesWithUndeclaredGraphQlOperation +{ + #[QueryParameter(key: 'search', description: 'Search by name', operations: [new Mutation(name: 'create')])] + public string $name = ''; +}