Skip to content

Commit beb070a

Browse files
committed
fix(openapi): dedup legacy filter deprecations
getFiltersParameters() looped every filter description entry and re-emitted the same swagger/openapi deprecation per parameter, so a filter exposing N params warned N times per doc generation. Guard the two triggers with a per-invocation $warned set keyed by filter + shortName so each fires at most once per generation. Establishes the F7 dedup convention (natural boundaries + local guard, no global static state) that the upcoming #[ApiFilter] (F5) and Operation::$filters (F6) runtime deprecations reuse.
1 parent d721d2d commit beb070a

2 files changed

Lines changed: 91 additions & 5 deletions

File tree

src/OpenApi/Factory/OpenApiFactory.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -702,27 +702,33 @@ private function getFiltersParameters(CollectionOperationInterface|HttpOperation
702702
$resourceFilters = $operation->getFilters();
703703
$entityClass = $this->getStateOptionsClass($operation, $operation->getClass());
704704

705+
// Collapse identical filter deprecations to one warning per (filter, shortName) per
706+
// generation: a filter exposing N parameters must not emit the same message N times.
707+
$warned = [];
705708
foreach ($resourceFilters ?? [] as $filterId) {
706709
if (!$this->filterLocator->has($filterId)) {
707710
continue;
708711
}
709712

710713
$filter = $this->filterLocator->get($filterId);
711714
foreach ($filter->getDescription($entityClass) as $name => $description) {
712-
$parameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $filterId);
715+
$parameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $filterId, $warned);
713716
}
714717
}
715718

716719
return $parameters;
717720
}
718721

719722
/**
720-
* @param array<string, mixed> $description
723+
* @param array<string, mixed> $description
724+
* @param array<string, array<string, bool>> $warned
721725
*/
722-
private function getFilterParameter(string $name, array $description, string $shortName, string $filter): Parameter
726+
private function getFilterParameter(string $name, array $description, string $shortName, string $filter, array &$warned = []): Parameter
723727
{
724-
if (isset($description['swagger'])) {
728+
$warnKey = $filter.'::'.$shortName;
729+
if (isset($description['swagger']) && !isset($warned[$warnKey]['swagger'])) {
725730
trigger_deprecation('api-platform/core', '4.0', \sprintf('Using the "swagger" field of the %s::getDescription() (%s) is deprecated.', $filter, $shortName));
731+
$warned[$warnKey]['swagger'] = true;
726732
}
727733

728734
if (!isset($description['openapi']) || $description['openapi'] instanceof Parameter) {
@@ -771,7 +777,10 @@ private function getFilterParameter(string $name, array $description, string $sh
771777
return $parameter->withSchema($schema);
772778
}
773779

774-
trigger_deprecation('api-platform/core', '4.0', \sprintf('Not using "%s" on the "openapi" field of the %s::getDescription() (%s) is deprecated.', Parameter::class, $filter, $shortName));
780+
if (!isset($warned[$warnKey]['openapi'])) {
781+
trigger_deprecation('api-platform/core', '4.0', \sprintf('Not using "%s" on the "openapi" field of the %s::getDescription() (%s) is deprecated.', Parameter::class, $filter, $shortName));
782+
$warned[$warnKey]['openapi'] = true;
783+
}
775784

776785
$schema = $description['schema'] ?? null;
777786

src/OpenApi/Tests/Factory/OpenApiFactoryTest.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,4 +1498,81 @@ public function testMetadataParameterInOpenApiOperationParametersThrows(): void
14981498

14991499
$factory->__invoke();
15001500
}
1501+
1502+
public function testLegacyFilterDeprecationFiresOncePerFilterAndShortName(): void
1503+
{
1504+
$resourceNameCollectionFactory = $this->createMock(ResourceNameCollectionFactoryInterface::class);
1505+
$resourceCollectionMetadataFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
1506+
$propertyNameCollectionFactory = $this->createMock(PropertyNameCollectionFactoryInterface::class);
1507+
$propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class);
1508+
$definitionNameFactory = new DefinitionNameFactory([]);
1509+
1510+
$resourceCollectionMetadata = new ResourceMetadataCollection(Dummy::class, [(new ApiResource(operations: [
1511+
(new GetCollection())
1512+
->withClass(Dummy::class)
1513+
->withShortName('Dummy')
1514+
->withName('api_dummies_get_collection')
1515+
->withUriTemplate('/dummies')
1516+
->withFilters(['legacy_filter']),
1517+
]))->withClass(Dummy::class)]);
1518+
1519+
$resourceCollectionMetadataFactory
1520+
->method('create')
1521+
->willReturnCallback(static fn (string $resourceClass): ResourceMetadataCollection => match ($resourceClass) {
1522+
Dummy::class => $resourceCollectionMetadata,
1523+
default => new ResourceMetadataCollection($resourceClass, []),
1524+
});
1525+
1526+
$resourceNameCollectionFactory->method('create')->willReturn(new ResourceNameCollection([Dummy::class]));
1527+
$propertyNameCollectionFactory->method('create')->willReturn(new PropertyNameCollection([]));
1528+
1529+
// A legacy filter exposing several parameters, each carrying the deprecated "swagger" field.
1530+
$filter = new DummyFilter([
1531+
'name' => ['property' => 'name', 'type' => 'string', 'required' => false, 'swagger' => ['type' => 'string']],
1532+
'foo' => ['property' => 'foo', 'type' => 'string', 'required' => false, 'swagger' => ['type' => 'string']],
1533+
'bar' => ['property' => 'bar', 'type' => 'string', 'required' => false, 'swagger' => ['type' => 'string']],
1534+
]);
1535+
1536+
$filterLocator = $this->createMock(ContainerInterface::class);
1537+
$filterLocator->method('has')->willReturnCallback(static fn (string $id): bool => 'legacy_filter' === $id);
1538+
$filterLocator->method('get')->willReturn($filter);
1539+
1540+
$schemaFactory = new SchemaFactory(
1541+
resourceMetadataFactory: $resourceCollectionMetadataFactory,
1542+
propertyNameCollectionFactory: $propertyNameCollectionFactory,
1543+
propertyMetadataFactory: $propertyMetadataFactory,
1544+
nameConverter: new CamelCaseToSnakeCaseNameConverter(),
1545+
definitionNameFactory: $definitionNameFactory,
1546+
);
1547+
1548+
$factory = new OpenApiFactory(
1549+
$resourceNameCollectionFactory,
1550+
$resourceCollectionMetadataFactory,
1551+
$propertyNameCollectionFactory,
1552+
$propertyMetadataFactory,
1553+
$schemaFactory,
1554+
$filterLocator,
1555+
[],
1556+
new Options('Test API', 'This is a test API.', '1.2.3'),
1557+
new PaginationOptions(),
1558+
null,
1559+
['json' => ['application/problem+json']]
1560+
);
1561+
1562+
$deprecations = [];
1563+
set_error_handler(static function (int $type, string $message) use (&$deprecations): bool {
1564+
$deprecations[] = $message;
1565+
1566+
return true;
1567+
}, \E_USER_DEPRECATED);
1568+
1569+
try {
1570+
$factory->__invoke();
1571+
} finally {
1572+
restore_error_handler();
1573+
}
1574+
1575+
$swaggerDeprecations = array_filter($deprecations, static fn (string $m): bool => str_contains($m, 'Using the "swagger" field'));
1576+
$this->assertCount(1, $swaggerDeprecations, 'The "swagger" deprecation must fire once per filter and short name, not once per filter parameter.');
1577+
}
15011578
}

0 commit comments

Comments
 (0)