From 2c82381c4f17d3e3edaffb9241c6997272787697 Mon Sep 17 00:00:00 2001 From: Alexis Lefebvre Date: Tue, 21 Jul 2026 17:14:37 +0200 Subject: [PATCH] fix(metadata): downgrade inline unwired-filter warmup log from alert to debug An inline filter instance passed to a query parameter is never wired with a manager registry, unlike a filter resolved as a service. Reading its legacy description during cache warmup then fails on the Doctrine metadata lookup. This is expected and recoverable, yet it was logged at alert level, spamming one line per such parameter on every cache clear and misleading unrelated diagnostics. Log this known warmup case at debug level, keeping alert for genuinely unexpected registry failures. Closes #7361 Co-Authored-By: Claude Opus 4.8 (1M context) --- ...meterResourceMetadataCollectionFactory.php | 12 +- ...rResourceMetadataCollectionFactoryTest.php | 118 ++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) 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