Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/Dumbo.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ public function handle(ServerRequestInterface $request): ResponseInterface
}

try {
$route = $this->router->findRoute($request);
$dispatch = $this->router->dispatch($request);
$route = $dispatch["route"];

$context = new Context(
$request,
Expand All @@ -215,6 +216,16 @@ public function handle(ServerRequestInterface $request): ResponseInterface
);

$handler = $route["handler"];
} elseif ($dispatch["allowedMethods"] !== []) {
$allowed = implode(", ", $dispatch["allowedMethods"]);

$handler = function () use ($allowed) {
return new Response(
405,
["Allow" => $allowed],
"405 Method Not Allowed"
);
};
} else {
$handler = function () {
return new Response(404, [], "404 Not Found");
Expand Down
39 changes: 31 additions & 8 deletions src/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,15 @@ public function addGroup(string $prefix, array $groupRoutes): void
}

/**
* Find a matching route for the given request
* Dispatch the given request against the registered routes
*
* When the path is registered but not for the request method, the route is
* null and the methods that path does accept are returned instead.
*
* @param ServerRequestInterface $request The incoming HTTP request
* @return array|null The matched route information or null if no match found
* @return array{route: array|null, allowedMethods: array<string>} The dispatch result
*/
public function findRoute(ServerRequestInterface $request): ?array
public function dispatch(ServerRequestInterface $request): array
{
if (!$this->dispatcher) {
$this->buildDispatcher();
Expand All @@ -79,14 +82,34 @@ public function findRoute(ServerRequestInterface $request): ?array
$vars = $routeInfo[2];

return [
"handler" => $handler["handler"],
"params" => $vars,
"routePath" => $handler["path"],
"middleware" => $handler["middleware"] ?? [],
"route" => [
"handler" => $handler["handler"],
"params" => $vars,
"routePath" => $handler["path"],
"middleware" => $handler["middleware"] ?? [],
],
"allowedMethods" => [],
];
}

return null;
return [
"route" => null,
"allowedMethods" =>
$routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED
? array_values(array_unique($routeInfo[1]))
: [],
];
}

/**
* Find a matching route for the given request
*
* @param ServerRequestInterface $request The incoming HTTP request
* @return array|null The matched route information or null if no match found
*/
public function findRoute(ServerRequestInterface $request): ?array
{
return $this->dispatch($request)["route"];
}

/**
Expand Down
57 changes: 57 additions & 0 deletions tests/DumboTest.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?php

use PHPUnit\Framework\TestCase;
use Dumbo\Context;
use Dumbo\Dumbo;
use GuzzleHttp\Psr7\ServerRequest;

class DumboTest extends TestCase
{
Expand Down Expand Up @@ -77,4 +79,59 @@ public function testErrorReportingConfiguration()
$this->assertEquals(E_ALL, error_reporting());
$this->assertEquals("1", ini_get("display_errors"));
}

public function testMethodNotAllowedReturns405WithAllowHeader()
{
$app = new Dumbo();
$app->get("/users", fn(Context $context) => $context->text("users"));
$app->post("/users", fn(Context $context) => $context->text("created"));

$response = $app->handle(new ServerRequest("DELETE", "/users"));

$this->assertEquals(405, $response->getStatusCode());
$this->assertEquals("GET, POST", $response->getHeaderLine("Allow"));
$this->assertEquals(
"405 Method Not Allowed",
(string) $response->getBody()
);
}

public function testUnknownPathStillReturns404()
{
$app = new Dumbo();
$app->get("/users", fn(Context $context) => $context->text("users"));

$response = $app->handle(new ServerRequest("GET", "/nope"));

$this->assertEquals(404, $response->getStatusCode());
$this->assertFalse($response->hasHeader("Allow"));
$this->assertEquals("404 Not Found", (string) $response->getBody());
}

public function testHeadRequestFallsBackToGetRoute()
{
$app = new Dumbo();
$app->get("/users", fn(Context $context) => $context->text("users"));

$response = $app->handle(new ServerRequest("HEAD", "/users"));

$this->assertEquals(200, $response->getStatusCode());
}

public function testMiddlewareRunsForMethodNotAllowed()
{
$app = new Dumbo();
$app->use(
fn(Context $context, callable $next) => $next($context)->withHeader(
"X-Middleware",
"ran"
)
);
$app->get("/users", fn(Context $context) => $context->text("users"));

$response = $app->handle(new ServerRequest("DELETE", "/users"));

$this->assertEquals(405, $response->getStatusCode());
$this->assertEquals("ran", $response->getHeaderLine("X-Middleware"));
}
}
45 changes: 45 additions & 0 deletions tests/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,51 @@ public function testRouteNotFound()
$this->assertNull($route);
}

public function testDispatchReportsAllowedMethodsForMethodMismatch()
{
$handler = function (Context $context) {
return $context->text("Users");
};

$this->router->addRoute("GET", "/users", $handler);
$this->router->addRoute("POST", "/users", $handler);

$dispatch = $this->router->dispatch(
new ServerRequest("DELETE", "/users")
);

$this->assertNull($dispatch["route"]);
$this->assertEquals(["GET", "POST"], $dispatch["allowedMethods"]);
}

public function testDispatchReportsNoAllowedMethodsForUnknownPath()
{
$dispatch = $this->router->dispatch(
new ServerRequest("GET", "/non-existent-route")
);

$this->assertNull($dispatch["route"]);
$this->assertEquals([], $dispatch["allowedMethods"]);
}

public function testDispatchReturnsMatchedRoute()
{
$this->router->addRoute("GET", "/users/:id", function (
Context $context
) {
return $context->text("User");
});

$dispatch = $this->router->dispatch(
new ServerRequest("GET", "/users/123")
);

$this->assertNotNull($dispatch["route"]);
$this->assertEquals("/users/:id", $dispatch["route"]["routePath"]);
$this->assertEquals(["id" => "123"], $dispatch["route"]["params"]);
$this->assertEquals([], $dispatch["allowedMethods"]);
}

public function testMultipleMiddleware()
{
$middleware1 = function (Context $context, callable $next) {
Expand Down
Loading