diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php index 3972b1e8978..8f018b0885c 100644 --- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Metadata\Resource\Factory; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\Exception\RuntimeException; @@ -422,7 +423,16 @@ private function setDefaults(string $key, Parameter $parameter, ?object $filter, try { return $this->getLegacyFilterMetadata($parameter, $operation, $filter); } catch (RuntimeException $exception) { - $this->logger?->alert($exception->getMessage(), ['exception' => $exception]); + // An inline filter instance (e.g. `new NumericFilter()`) passed to a QueryParameter is + // never wired with a ManagerRegistry, unlike a filter resolved through the filter locator + // as a service. Its getDescription() then fails on the Doctrine metadata lookup. This is + // expected and recoverable during cache warmup, so log it at debug level instead of the + // alarming ALERT reserved for genuinely unexpected registry failures. + if ($filter instanceof ManagerRegistryAwareInterface && !$filter->hasManagerRegistry()) { + $this->logger?->debug($exception->getMessage(), ['exception' => $exception]); + } else { + $this->logger?->alert($exception->getMessage(), ['exception' => $exception]); + } return $parameter; } diff --git a/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php index 1a54b664ca1..868f6ffc17b 100644 --- a/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php @@ -13,8 +13,11 @@ namespace ApiPlatform\Metadata\Tests\Resource\Factory; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\FilterInterface; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Parameters; @@ -29,6 +32,7 @@ use ApiPlatform\OpenApi\Model\Parameter; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\TypeInfo\Type; @@ -460,6 +464,120 @@ public function testSimplePropertyHasNoNestedPropertyInfo(): void $this->assertNotNull($param); $this->assertArrayNotHasKey('nested_properties_info', $param->getExtraProperties()); } + + /** + * An inline filter instance (e.g. `new NumericFilter()`) passed to a QueryParameter is never wired + * with a ManagerRegistry. Its getDescription() then throws during cache warmup: this is expected and + * recoverable, so it must be logged at debug level rather than the alarming ALERT (see issue #7361). + */ + public function testInlineRegistryAwareFilterFailureIsLoggedAtDebug(): 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); + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->never())->method('alert'); + $logger->expects($this->once())->method('debug'); + + $parameterFactory = new ParameterResourceMetadataCollectionFactory( + $nameCollection, + $propertyMetadata, + new AttributesResourceMetadataCollectionFactory(), + $filterLocator, + null, + $logger, + ); + + $parameterFactory->create(HasInlineRegistryAwareFilterParameter::class); + } + + /** + * A filter that is not ManagerRegistry-aware (or is already wired) but still throws during + * getDescription() reflects an unexpected condition and must keep the ALERT level. + */ + public function testNonRegistryAwareFilterFailureIsLoggedAtAlert(): 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); + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->once())->method('alert'); + $logger->expects($this->never())->method('debug'); + + $parameterFactory = new ParameterResourceMetadataCollectionFactory( + $nameCollection, + $propertyMetadata, + new AttributesResourceMetadataCollectionFactory(), + $filterLocator, + null, + $logger, + ); + + $parameterFactory->create(HasThrowingFilterParameter::class); + } +} + +class InlineRegistryAwareThrowingFilter implements FilterInterface, ManagerRegistryAwareInterface +{ + use ManagerRegistryAwareTrait; + + public function getDescription(string $resourceClass): array + { + // Mimics an unwired Doctrine AbstractFilter reaching the metadata during cache warmup. + $this->getManagerRegistry(); + + return []; + } +} + +class ThrowingFilter implements FilterInterface +{ + public function getDescription(string $resourceClass): array + { + throw new RuntimeException('boom'); + } +} + +#[ApiResource( + operations: [ + new GetCollection( + parameters: [ + 'name' => new QueryParameter(filter: new InlineRegistryAwareThrowingFilter()), + ] + ), + ] +)] +class HasInlineRegistryAwareFilterParameter +{ + public $id; + public $name; +} + +#[ApiResource( + operations: [ + new GetCollection( + parameters: [ + 'name' => new QueryParameter(filter: new ThrowingFilter()), + ] + ), + ] +)] +class HasThrowingFilterParameter +{ + public $id; + public $name; } class RepeatedPlaceholderExactFilter implements FilterInterface