diff --git a/src/JsonApi/State/JsonApiProvider.php b/src/JsonApi/State/JsonApiProvider.php index 0976d9ce84..c09c2fdb68 100644 --- a/src/JsonApi/State/JsonApiProvider.php +++ b/src/JsonApi/State/JsonApiProvider.php @@ -15,6 +15,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\Util\RequestParser; final class JsonApiProvider implements ProviderInterface { @@ -87,6 +88,16 @@ public function provide(Operation $operation, array $uriVariables = [], array $c } if ($filters) { + // ReadProvider skips its raw-query fallback when _api_filters is set, + // so preserve flat custom params here too. JSON:API transforms win. + $rawParams = $request->attributes->get('_api_query_parameters'); + if (null === $rawParams) { + $queryString = RequestParser::getQueryString($request); + $rawParams = $queryString ? RequestParser::parseRequestParams($queryString) : []; + $request->attributes->set('_api_query_parameters', $rawParams); + } + $filters = array_replace($rawParams, $filters); + $request->attributes->set('_api_filters', $filters); } diff --git a/src/JsonApi/Tests/State/JsonApiProviderTest.php b/src/JsonApi/Tests/State/JsonApiProviderTest.php index 6e5999bbdb..e315e8dbb2 100644 --- a/src/JsonApi/Tests/State/JsonApiProviderTest.php +++ b/src/JsonApi/Tests/State/JsonApiProviderTest.php @@ -55,4 +55,47 @@ public function testProvideMergesFlatPaginationWithBracketFilter(): void 'pagination' => 'true', ], $request->attributes->get('_api_filters')); } + + // #8216: flat custom params must survive when _api_filters is pre-set by JSON:API. + public function testProvidePreservesFlatCustomQueryParamsWithoutBracketFilter(): void + { + $request = Request::create('/sessions?city_id=3152&order[distance]=asc&page=1'); + $request->setRequestFormat('jsonapi'); + + $operation = new Get(class: \stdClass::class, shortName: 'dummy'); + $context = ['request' => $request]; + $decorated = $this->createMock(ProviderInterface::class); + $decorated->expects($this->once())->method('provide')->with($operation, [], $context); + + $provider = new JsonApiProvider($decorated); + $provider->provide($operation, [], $context); + + $filters = $request->attributes->get('_api_filters'); + + $this->assertIsArray($filters); + $this->assertSame('3152', $filters['city_id'] ?? null); + $this->assertSame(['distance' => 'asc'], $filters['order'] ?? null); + $this->assertSame('1', $filters['page'] ?? null); + } + + // _api_query_parameters set by an earlier listener / ParameterProvider must be reused. + public function testProvideHonoursPrePopulatedApiQueryParameters(): void + { + $request = Request::create('/sessions?page=1'); + $request->setRequestFormat('jsonapi'); + $request->attributes->set('_api_query_parameters', ['custom_override' => 'yes', 'page' => '1']); + + $operation = new Get(class: \stdClass::class, shortName: 'dummy'); + $context = ['request' => $request]; + $decorated = $this->createMock(ProviderInterface::class); + $decorated->expects($this->once())->method('provide')->with($operation, [], $context); + + $provider = new JsonApiProvider($decorated); + $provider->provide($operation, [], $context); + + $filters = $request->attributes->get('_api_filters'); + + $this->assertSame('yes', $filters['custom_override'] ?? null); + $this->assertSame('1', $filters['page'] ?? null); + } } diff --git a/tests/Functional/JsonApiFlatPaginationTest.php b/tests/Functional/JsonApiFlatPaginationTest.php index 8495531a2f..8766b2e1fa 100644 --- a/tests/Functional/JsonApiFlatPaginationTest.php +++ b/tests/Functional/JsonApiFlatPaginationTest.php @@ -70,6 +70,26 @@ public function testFlatPageWithBracketFilterDrivesCurrentPage(): void $this->assertSame(5, $data['meta']['totalItems'] ?? null); } + // #8216: flat filter param combined with flat page must still drive filtering. + public function testFlatCustomParamWithFlatPagePreservesFilter(): void + { + if ($this->isMongoDB()) { + $this->markTestSkipped('Not tested with mongodb.'); + } + + $response = self::createClient()->request('GET', '/dummies?name=foo&page=1', [ + 'headers' => ['Accept' => 'application/vnd.api+json'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('content-type', 'application/vnd.api+json; charset=utf-8'); + + $data = $response->toArray(); + + $this->assertSame(1, $data['meta']['currentPage'] ?? null); + $this->assertSame(5, $data['meta']['totalItems'] ?? null); + } + private function loadFixtures(): void { $manager = $this->getManager(); @@ -80,6 +100,10 @@ private function loadFixtures(): void $manager->persist($dummy); } + $bar = new Dummy(); + $bar->setName('bar'); + $manager->persist($bar); + $manager->flush(); } }