diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php index 3e62829e68..c39c4aab46 100644 --- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php @@ -69,12 +69,14 @@ public function create(string $resourceClass): ResourceMetadataCollection { $resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass); + $declaredOperationClasses = $this->getDeclaredOperationClasses($resourceMetadataCollection); + foreach ($resourceMetadataCollection as $i => $resource) { $operations = $resource->getOperations(); $internalPriority = -1; foreach ($operations as $operationName => $operation) { - $parameters = $this->getDefaultParameters($operation, $resourceClass, $internalPriority); + $parameters = $this->getDefaultParameters($operation, $resourceClass, $internalPriority, $declaredOperationClasses); if (\count($parameters) > 0) { $operations->add($operationName, $operation->withParameters($parameters)); } @@ -88,7 +90,7 @@ public function create(string $resourceClass): ResourceMetadataCollection $internalPriority = -1; foreach ($graphQlOperations as $operationName => $operation) { - $parameters = $this->getDefaultParameters($operation, $resourceClass, $internalPriority); + $parameters = $this->getDefaultParameters($operation, $resourceClass, $internalPriority, $declaredOperationClasses); if (\count($parameters) > 0) { $graphQlOperations[$operationName] = $operation->withParameters($parameters); } @@ -229,12 +231,15 @@ private function getProperties(string $resourceClass, ?Parameter $parameter = nu return $this->localPropertyCache[$k]; } - private function getDefaultParameters(Operation $operation, string $resourceClass, int &$internalPriority): Parameters + /** + * @param list> $declaredOperationClasses + */ + private function getDefaultParameters(Operation $operation, string $resourceClass, int &$internalPriority, array $declaredOperationClasses): Parameters { $propertyNames = $properties = []; $parameters = $operation->getParameters() ?? new Parameters(); - foreach ($this->createParametersFromAttributes($operation) as $key => $parameter) { + foreach ($this->createParametersFromAttributes($operation, $declaredOperationClasses) as $key => $parameter) { $parameters->add($key, $parameter); } @@ -510,7 +515,10 @@ private function getFilterInstance(object|string|null $filter): ?object return $this->filterLocator->get($filter); } - private function createParametersFromAttributes(Operation $operation): Parameters + /** + * @param list> $declaredOperationClasses + */ + private function createParametersFromAttributes(Operation $operation, array $declaredOperationClasses): Parameters { $parameters = new Parameters(); @@ -521,19 +529,22 @@ private function createParametersFromAttributes(Operation $operation): Parameter foreach ((new \ReflectionClass($resourceClass))->getProperties() as $reflectionProperty) { foreach ($reflectionProperty->getAttributes(Parameter::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { $parameter = $attribute->newInstance(); + $propertyName = $reflectionProperty->getName(); - if ( - null !== ($parameterOperations = $parameter->getOperations()) - && !\in_array( - $operation::class, - array_map(static fn ($parameterOperation) => $parameterOperation::class, $parameterOperations), - true - ) - ) { - continue; + if (null !== ($parameterOperations = $parameter->getOperations())) { + $parameterOperationClasses = array_map(static fn ($parameterOperation) => $parameterOperation::class, $parameterOperations); + + foreach ($parameterOperationClasses as $parameterOperationClass) { + if (!\in_array($parameterOperationClass, $declaredOperationClasses, true)) { + throw new RuntimeException(\sprintf('Parameter attribute on property "%s" is restricted to the operation "%s" which is not declared on the resource "%s".', $propertyName, $parameterOperationClass, $resourceClass)); + } + } + + if (!\in_array($operation::class, $parameterOperationClasses, true)) { + continue; + } } - $propertyName = $reflectionProperty->getName(); $key = $parameter->getKey() ?? $propertyName; if (null === $parameterPropertyName = $parameter->getProperty()) { @@ -554,4 +565,19 @@ private function createParametersFromAttributes(Operation $operation): Parameter return $parameters; } + + /** + * @return list> + */ + private function getDeclaredOperationClasses(ResourceMetadataCollection $resourceMetadataCollection): array + { + $operationClasses = []; + foreach ($resourceMetadataCollection as $resource) { + foreach ($resource->getOperations() ?? [] 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 a3f6de7832..400927f385 100644 --- a/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php @@ -819,8 +819,11 @@ public function testHeaderParameterFromPropertyAttributePropertiesHasMultipleInc $this->assertSame(['authToken'], $authParam->getProperties()); } - public function testQueryParameterOnPropertiesWithOperations(): void + public function testQueryParameterOnPropertiesThrowsExceptionWhenOperationIsNotDeclared(): void { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(\sprintf('Parameter attribute on property "id" is restricted to the operation "%s" which is not declared on the resource "%s".', Patch::class, QueryParameterOnPropertiesWithOperations::class)); + $nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class); $nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'name'])); @@ -839,22 +842,7 @@ public function testQueryParameterOnPropertiesWithOperations(): void $filterLocator ); - $resourceMetadataCollection = $parameterFactory->create(QueryParameterOnPropertiesWithOperations::class); - $operations = array_values(iterator_to_array($resourceMetadataCollection[0]->getOperations())); - - $this->assertCount(2, $operations); - - $collectionOperation = $operations[0]; - $this->assertInstanceOf(GetCollection::class, $collectionOperation); - $collectionParameters = $collectionOperation->getParameters(); - $this->assertTrue($collectionParameters->has('search')); - $this->assertFalse($collectionParameters->has('filter_id')); - - $getOperation = $operations[1]; - $this->assertInstanceOf(Get::class, $getOperation); - $getParameters = $getOperation->getParameters(); - $this->assertTrue($getParameters->has('search')); - $this->assertTrue($getParameters->has('filter_id')); + $parameterFactory->create(QueryParameterOnPropertiesWithOperations::class); } public function testHeaderParameterOnPropertiesWithOperations(): void