From 4dd2a76cb274fba0c024a73132ec838d4d318769 Mon Sep 17 00:00:00 2001 From: Maxcastel Date: Thu, 1 Oct 2026 17:03:21 +0200 Subject: [PATCH 1/2] fix(metadata): skip parameter attributes on properties already declared on the operation --- .../Factory/ParameterResourceMetadataCollectionFactory.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php index f365ba512a..8fe0eb6f2d 100644 --- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php @@ -241,6 +241,10 @@ private function getDefaultParameters(Operation $operation, string $resourceClas $parameters = $existingParameters ? clone $existingParameters : new Parameters(); foreach ($this->createParametersFromAttributes($operation, $declaredOperationClasses) as $key => $parameter) { + if (!str_contains($key, ':property') && $parameters->has($key, $parameter::class)) { + continue; + } + $parameters->add($key, $parameter); } From d26f0c41aef0a4a2e929a30e46ff947af6487d45 Mon Sep 17 00:00:00 2001 From: Maxcastel Date: Thu, 1 Oct 2026 17:04:44 +0200 Subject: [PATCH 2/2] test(metadata): cover operation parameters overriding parameter attributes on properties --- ...rResourceMetadataCollectionFactoryTest.php | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php index 4ba89cc49a..8b9540f326 100644 --- a/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php @@ -1046,6 +1046,45 @@ public function testParameterOnPropertiesThrowsExceptionWhenOperationClassString $parameterFactory->create(ParameterOnPropertiesWithUndeclaredOperationClassString::class); } + public function testOperationParameterTakesPrecedenceOverParameterAttributeOnProperty(): void + { + $nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class); + $nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', '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(), + $filterLocator + ); + + $resourceMetadataCollection = $parameterFactory->create(QueryParameterOnPropertiesOverriddenByOperation::class); + $operations = array_values(iterator_to_array($resourceMetadataCollection[0]->getOperations())); + + $this->assertCount(2, $operations); + + [$collectionOperation, $getOperation] = $operations; + $this->assertInstanceOf(GetCollection::class, $collectionOperation); + $collectionParameter = $collectionOperation->getParameters()->get('name'); + $this->assertInstanceOf(QueryParameter::class, $collectionParameter); + $this->assertSame('From operation', $collectionParameter->getDescription()); + $this->assertFalse($collectionParameter->getRequired()); + + $this->assertInstanceOf(Get::class, $getOperation); + $getParameter = $getOperation->getParameters()->get('name'); + $this->assertInstanceOf(QueryParameter::class, $getParameter); + $this->assertSame('From property', $getParameter->getDescription()); + $this->assertTrue($getParameter->getRequired()); + } + public function testNestedPropertyWithNameConverter(): void { $nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class); @@ -1631,3 +1670,17 @@ class ParameterOnPropertiesWithUndeclaredOperationClassString #[QueryParameter(key: 'search', description: 'Search by name', operations: [Patch::class])] public string $name = ''; } + +#[ApiResource( + operations: [ + new GetCollection(parameters: ['name' => new QueryParameter(description: 'From operation', required: false)]), + new Get(), + ] +)] +class QueryParameterOnPropertiesOverriddenByOperation +{ + public int $id = 0; + + #[QueryParameter(description: 'From property', required: true)] + public string $name = ''; +}