diff --git a/src/App.php b/src/App.php
index c746ceb..0572933 100644
--- a/src/App.php
+++ b/src/App.php
@@ -25,6 +25,12 @@ class App
/** @var ReactiveHandler|SapiHandler */
private $sapi;
+ /** @var string */
+ protected $currentGroupPrefix;
+
+ /** @var callable|class-string[] */
+ protected $currentGroupHandlers = [];
+
/**
* Instantiate new X application
*
@@ -190,7 +196,27 @@ public function options(string $route, $handler, ...$handlers): void
*/
public function any(string $route, $handler, ...$handlers): void
{
- $this->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $route, $handler, ...$handlers);
+ $this->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $this->currentGroupPrefix . $route, $handler, ...$handlers);
+ }
+
+ /**
+ * Create a route group with a common prefix.
+ *
+ * All routes created in the passed callback will have the given group prefix prepended.
+ *
+ * @param string $prefix
+ * @param callable|class-string[] $handlers
+ * @param callable $callback
+ */
+ public function addGroup(string $prefix, array $handlers, callable $callback): void
+ {
+ $previousGroupPrefix = $this->currentGroupPrefix;
+ $previousGroupHandlers = $this->currentGroupHandlers;
+ $this->currentGroupPrefix = $previousGroupPrefix . $prefix;
+ $this->currentGroupHandlers = array_merge($previousGroupHandlers, $handlers);
+ $callback($this);
+ $this->currentGroupPrefix = $previousGroupPrefix;
+ $this->currentGroupHandlers = $previousGroupHandlers;
}
/**
@@ -202,7 +228,13 @@ public function any(string $route, $handler, ...$handlers): void
*/
public function map(array $methods, string $route, $handler, ...$handlers): void
{
- $this->router->map($methods, $route, $handler, ...$handlers);
+ if (!empty($this->currentGroupHandlers)) {
+ \array_unshift($handlers, $handler);
+ $currentGroupHandlers = $this->currentGroupHandlers;
+ $handler = array_pop($currentGroupHandlers);
+ \array_unshift($handlers, ...$currentGroupHandlers);
+ }
+ $this->router->map($methods, $this->currentGroupPrefix . $route, $handler, ...$handlers);
}
/**
diff --git a/tests/AppTest.php b/tests/AppTest.php
index d59955d..0d29bea 100644
--- a/tests/AppTest.php
+++ b/tests/AppTest.php
@@ -1649,6 +1649,210 @@ public function testInvokeWithMatchingRouteReturnsInternalServerErrorResponseWhe
$this->assertStringContainsString("
Expected request handler to return Psr\Http\Message\ResponseInterface
but got null
.
\n", (string) $response->getBody());
}
+ public function testInvokeWithMatchingGroupRouteReturnsResponseFromMatchingRouteHandler(): void
+ {
+ $app = $this->createAppWithoutLogger();
+ $app->addGroup('/users', [], function ($app) {
+ $app->get('', function () {
+ return new Response(
+ 200,
+ [
+ 'Content-Type' => 'text/html'
+ ],
+ "OK\n"
+ );
+ });
+ });
+ $request = new ServerRequest('GET', 'http://localhost/users');
+
+ $response = $app($request);
+ assert($response instanceof ResponseInterface);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('text/html', $response->getHeaderLine('Content-Type'));
+ $this->assertEquals("OK\n", (string) $response->getBody());
+ }
+
+ public function testInvokeWithMatchingChildGroupRouteReturnsResponseFromMatchingRouteHandler(): void
+ {
+ $app = $this->createAppWithoutLogger();
+ $app->addGroup('/users', [], function ($app) {
+ $app->addGroup('/{name}', [], function ($app) {
+ $app->get('/posts/{post}', function (ServerRequestInterface $request) {
+ $name = $request->getAttribute('name');
+ $post = $request->getAttribute('post');
+ assert(is_string($name));
+ assert(is_string($post));
+
+ return new Response(
+ 200,
+ [
+ 'Content-Type' => 'text/html'
+ ],
+ "OK $name $post\n"
+ );
+ });
+ });
+ });
+ $request = new ServerRequest('GET', 'http://localhost/users/alice/posts/first');
+
+ $response = $app($request);
+ assert($response instanceof ResponseInterface);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('text/html', $response->getHeaderLine('Content-Type'));
+ $this->assertEquals("OK alice first\n", (string) $response->getBody());
+ }
+
+ public function testConstractAndGroupRouteWithMiddlewareReturnContructMiddleware(): void
+ {
+
+ $middleware1 = function () {
+ return new Response(
+ 200,
+ [
+ 'Content-Type' => 'text/html'
+ ],
+ "OK1\n"
+ );
+ };
+ $middleware2 = function () {
+ return new Response(
+ 200,
+ [
+ 'Content-Type' => 'text/html'
+ ],
+ "OK2\n"
+ );
+ };
+
+ $app = $this->createAppWithoutLogger(
+ $middleware1
+ );
+ $app->addGroup('/users', [
+ $middleware2
+ ], function ($app) {
+ $app->get('', function () {
+ return new Response(
+ 200,
+ [
+ 'Content-Type' => 'text/html'
+ ],
+ "OK3\n"
+ );
+ });
+ });
+ $request = new ServerRequest('GET', 'http://localhost/users');
+
+ $response = $app($request);
+ assert($response instanceof ResponseInterface);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('text/html', $response->getHeaderLine('Content-Type'));
+ $this->assertEquals("OK1\n", (string) $response->getBody());
+ }
+
+ public function testGroupRouteWithMiddlewareReturnGroupMiddleware(): void
+ {
+
+ $middleware2 = function () {
+ return new Response(
+ 200,
+ [
+ 'Content-Type' => 'text/html'
+ ],
+ "OK2\n"
+ );
+ };
+
+ $app = $this->createAppWithoutLogger();
+ $app->addGroup('/users', [
+ $middleware2
+ ], function ($app) {
+ $app->get('', function () {
+ return new Response(
+ 200,
+ [
+ 'Content-Type' => 'text/html'
+ ],
+ "OK3\n"
+ );
+ });
+ });
+ $request = new ServerRequest('GET', 'http://localhost/users');
+
+ $response = $app($request);
+ assert($response instanceof ResponseInterface);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('text/html', $response->getHeaderLine('Content-Type'));
+ $this->assertEquals("OK2\n", (string) $response->getBody());
+ }
+
+ public function testGroupRouteWithMiddlewareReturnRouteMiddleware(): void
+ {
+
+ $middleware2 = function ($request, $next) {
+ return $next($request);
+ };
+
+ $app = $this->createAppWithoutLogger();
+ $app->addGroup('/users', [
+ $middleware2
+ ], function ($app) {
+ $app->get('', function () {
+ return new Response(
+ 200,
+ [
+ 'Content-Type' => 'text/html'
+ ],
+ "OK3\n"
+ );
+ });
+ });
+ $request = new ServerRequest('GET', 'http://localhost/users');
+
+ $response = $app($request);
+ assert($response instanceof ResponseInterface);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('text/html', $response->getHeaderLine('Content-Type'));
+ $this->assertEquals("OK3\n", (string) $response->getBody());
+ }
+
+ public function testSetRequestGroupRouteWithMiddlewareAndReturnRouteMiddleware(): void
+ {
+
+ $middleware2 = function ($request, $next) {
+ return $next($request->withAttribute('group', 'group'));
+ };
+
+ $app = $this->createAppWithoutLogger();
+ $app->addGroup('/users', [
+ $middleware2
+ ], function ($app) {
+ $app->get('', function ($request) {
+ $group = $request->getAttribute('group');
+ assert(is_string($group));
+ return new Response(
+ 200,
+ [
+ 'Content-Type' => 'text/html'
+ ],
+ "OK {$group}\n"
+ );
+ });
+ });
+ $request = new ServerRequest('GET', 'http://localhost/users');
+
+ $response = $app($request);
+ assert($response instanceof ResponseInterface);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('text/html', $response->getHeaderLine('Content-Type'));
+ $this->assertEquals("OK group\n", (string) $response->getBody());
+ }
+
private function createAppWithoutLogger(callable ...$middleware): App
{
$app = new App(...$middleware);