From eff557cf9d6a2280751c2e3b452a39adf6a81a5e Mon Sep 17 00:00:00 2001 From: purwantoid Date: Wed, 29 Jul 2026 16:15:05 +0700 Subject: [PATCH 1/2] Add `query()` route method for the HTTP QUERY verb (RFC 10008) The HTTP QUERY method was recently published as RFC 10008 on the IETF Standards Track. It is a safe, cacheable method that carries its query in the request body, enabling expressive queries without the URL-length and cacheability trade-offs of GET vs. POST. Support for QUERY is landing across the ecosystem (https://www.rfc-editor.org/rfc/rfc10008), so this adds a query() helper as a sibling of the existing get()/post()/put()/patch()/ delete()/options() route methods on RouteCollectorProxyInter implemented via the existing map() mechanism: ``` $app->query('/search', function (Request $request, Response $response) { $criteria = (string) $request->getBody(); // ...run the query described in the request body... return $response; }); ``` `$app->query()` is equivalent to `$app->map(['QUERY'], ...)` and is not added to any(), so existing any() routes are unaffected. --- CHANGELOG.md | 2 ++ .../RouteCollectorProxyInterface.php | 10 ++++++ Slim/Routing/RouteCollectorProxy.php | 8 +++++ tests/AppTest.php | 32 ++++++++++++++++++ tests/Routing/RouteCollectorProxyTest.php | 33 +++++++++++++++++++ 5 files changed, 85 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa4baed2f..8ff49bef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Added +- Add `query()` route helper for the HTTP `QUERY` method ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)) + ### Changed ### Removed diff --git a/Slim/Interfaces/RouteCollectorProxyInterface.php b/Slim/Interfaces/RouteCollectorProxyInterface.php index 1c49bb228..626d8d706 100644 --- a/Slim/Interfaces/RouteCollectorProxyInterface.php +++ b/Slim/Interfaces/RouteCollectorProxyInterface.php @@ -90,6 +90,16 @@ public function delete(string $pattern, $callable): RouteInterface; */ public function options(string $pattern, $callable): RouteInterface; + /** + * Add QUERY route + * + * @param string $pattern The route URI pattern + * @param callable|array{class-string, string}|string $callable The route callback routine + * + * @link https://www.rfc-editor.org/rfc/rfc10008 + */ + public function query(string $pattern, $callable): RouteInterface; + /** * Add route for any HTTP method * diff --git a/Slim/Routing/RouteCollectorProxy.php b/Slim/Routing/RouteCollectorProxy.php index a946d148f..28ecae4c8 100644 --- a/Slim/Routing/RouteCollectorProxy.php +++ b/Slim/Routing/RouteCollectorProxy.php @@ -151,6 +151,14 @@ public function options(string $pattern, $callable): RouteInterface return $this->map(['OPTIONS'], $pattern, $callable); } + /** + * {@inheritdoc} + */ + public function query(string $pattern, $callable): RouteInterface + { + return $this->map(['QUERY'], $pattern, $callable); + } + /** * {@inheritdoc} */ diff --git a/tests/AppTest.php b/tests/AppTest.php index f691de12b..504160e01 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -241,6 +241,38 @@ public function testAnyRoute(): void } } + public function testQueryRoute(): void + { + $streamProphecy = $this->prophesize(StreamInterface::class); + $streamProphecy->__toString()->willReturn('Hello World'); + + $responseProphecy = $this->prophesize(ResponseInterface::class); + $responseProphecy->getBody()->willReturn($streamProphecy->reveal()); + + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal()); + + $uriProphecy = $this->prophesize(UriInterface::class); + $uriProphecy->getPath()->willReturn('/'); + + $requestProphecy = $this->prophesize(ServerRequestInterface::class); + $requestProphecy->getMethod()->willReturn('QUERY'); + $requestProphecy->getUri()->willReturn($uriProphecy->reveal()); + $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null); + $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) { + $this->getAttribute($args[0])->willReturn($args[1]); + return $this; + }); + + $app = new App($responseFactoryProphecy->reveal()); + $app->query('/', function (ServerRequestInterface $request, ResponseInterface $response) { + return $response; + }); + $response = $app->handle($requestProphecy->reveal()); + + $this->assertSame('Hello World', (string) $response->getBody()); + } + /******************************************************************************** * Route collector proxy methods *******************************************************************************/ diff --git a/tests/Routing/RouteCollectorProxyTest.php b/tests/Routing/RouteCollectorProxyTest.php index 2b2843fa3..ab7937276 100644 --- a/tests/Routing/RouteCollectorProxyTest.php +++ b/tests/Routing/RouteCollectorProxyTest.php @@ -317,6 +317,39 @@ public function testOptions() $this->assertSame($pattern, $route->getPattern()); } + public function testQuery() + { + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class); + + $pattern = '/'; + $callable = function () { + }; + + $routeProphecy = $this->prophesize(RouteInterface::class); + $routeProphecy + ->getPattern() + ->willReturn($pattern) + ->shouldBeCalledOnce(); + + $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class); + $routeCollectorProphecy + ->map(['QUERY'], $pattern, Argument::is($callable)) + ->willReturn($routeProphecy->reveal()) + ->shouldBeCalledOnce(); + + $routeCollectorProxy = new RouteCollectorProxy( + $responseFactoryProphecy->reveal(), + $callableResolverProphecy->reveal(), + null, + $routeCollectorProphecy->reveal() + ); + + $route = $routeCollectorProxy->query($pattern, $callable); + + $this->assertSame($pattern, $route->getPattern()); + } + public function testAny() { $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); From 9b68a980b88e2fd8511f3df76f546ebcfc3e0fba Mon Sep 17 00:00:00 2001 From: purwantoid Date: Thu, 30 Jul 2026 13:11:48 +0700 Subject: [PATCH 2/2] Fix flaky AppFactoryTest by isolating testDetermineResponseFactoryThrowsRuntimeException --- tests/Factory/AppFactoryTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Factory/AppFactoryTest.php b/tests/Factory/AppFactoryTest.php index 42565e89f..0d467c14e 100644 --- a/tests/Factory/AppFactoryTest.php +++ b/tests/Factory/AppFactoryTest.php @@ -113,6 +113,7 @@ public function testDetermineResponseFactoryThrowsRuntimeExceptionIfDecoratedNot /** * @runInSeparateProcess - Psr17FactoryProvider::setFactories breaks other tests */ + #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] public function testDetermineResponseFactoryThrowsRuntimeException() { $this->expectException(RuntimeException::class);