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
9 changes: 9 additions & 0 deletions infra/Exceptions/MethodNotAllowedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Infra\Exceptions;

use Exception;

class MethodNotAllowedException extends Exception {}
9 changes: 1 addition & 8 deletions infra/Exceptions/NotFoundException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,5 @@
namespace Infra\Exceptions;

use Exception;
use Throwable;

class NotFoundException extends Exception
{
public function __construct(string $message = 'Route Not Found', int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
class NotFoundException extends Exception {}
17 changes: 17 additions & 0 deletions infra/Http/Handlers/MethodNotAllowedHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Infra\Http\Handlers;

use Infra\Http\Request;
use Infra\Http\Response;
use Infra\Interfaces\RequestHandlerInterface;

class MethodNotAllowedHandler implements RequestHandlerInterface
{
public function handle(Request $request): Response
{
return Response::json(['error' => 'Method Not Allowed'], 405);
}
}
44 changes: 29 additions & 15 deletions infra/Http/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,22 @@ class Route
/** @var array<string,float|int|string> */
private array $params;

private string $pattern;

public function __construct(
private readonly string $path,
private readonly HttpMethod $method,
private readonly RequestHandlerInterface $handler
) {
$this->params = [];
$this->pattern = $this->compilePath();
}

private function compilePath(): string
{
$pattern = preg_replace('/\{([a-zA-Z0-9_]+)}/', '(?P<$1>[^/]+)', $this->path);

return '#^'.$pattern.'$#';
}

public function getHandler(): RequestHandlerInterface
Expand All @@ -37,23 +47,10 @@ public function match(Request $request): bool
return false;
}

// Convert route path to a regex pattern
// e.g., /users/{id} becomes #^/users/(?P<id>[^/]+)$#
$pattern = preg_replace('/\{([a-zA-Z0-9_]+)}/', '(?P<$1>[^/]+)', $this->path);
$pattern = '#^'.$pattern.'$#';

if (preg_match($pattern, $request->getPath(), $matches)) {
// Extract named parameters
if (preg_match($this->pattern, $request->getPath(), $matches)) {
foreach ($matches as $key => $value) {
if (is_string($key)) {
if (filter_var($value, FILTER_VALIDATE_FLOAT)) {
$value = floatval($value);
}

if (filter_var($value, FILTER_VALIDATE_INT)) {
$value = intval($value);
}
$this->params[$key] = $value;
$this->extractParams($value, $key);
}
}

Expand All @@ -62,4 +59,21 @@ public function match(Request $request): bool

return false;
}

public function matchPath(Request $request): bool
{
return (bool) preg_match($this->pattern, $request->getPath());
}

private function extractParams(string $value, string $key): void
{
if (filter_var($value, FILTER_VALIDATE_FLOAT)) {
$value = floatval($value);
}

if (filter_var($value, FILTER_VALIDATE_INT)) {
$value = intval($value);
}
$this->params[$key] = $value;
}
}
16 changes: 15 additions & 1 deletion infra/Http/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
namespace Infra\Http;

use Infra\Enums\HttpMethod;
use Infra\Exceptions\MethodNotAllowedException;
use Infra\Exceptions\NotFoundException;
use Infra\Http\Handlers\MethodNotAllowedHandler;
use Infra\Http\Handlers\NotFoundHandler;
use Infra\Http\Handlers\RedirectHandler;
use Infra\Interfaces\RequestHandlerInterface;
Expand All @@ -15,9 +17,10 @@ class Router
/** @var Route[] */
private array $routes = [];

private bool $pathMatched = false;

/**
* @todo handle not authorized requests
* @todo handle method not allowed request
*/
public function __construct(
private readonly Request $request,
Expand Down Expand Up @@ -66,20 +69,31 @@ public function handleRequest(): Response
$response = $route->getHandler()->handle($this->request);
} catch (NotFoundException) {
$response = (new NotFoundHandler)->handle($this->request);
} catch (MethodNotAllowedException) {
$response = (new MethodNotAllowedHandler)->handle($this->request);
}

return $response;
}

/**
* @throws NotFoundException
* @throws MethodNotAllowedException
*/
private function getRoute(): Route
{
foreach ($this->routes as $route) {
if ($route->match($this->request)) {
return $route;
}

if ($route->matchPath($this->request)) {
$this->pathMatched = true;
}
}

if ($this->pathMatched) {
throw new MethodNotAllowedException;
}

throw new NotFoundException;
Expand Down
4 changes: 2 additions & 2 deletions tests/Unit/infra/Http/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@
expect($response->getStatus())->toBe(404);
});

it('should return a 404 response when the path matches but the HTTP method does not', function () {
it('should return a 405 response when the path matches but the HTTP method does not', function () {
$request = new Request('/test', HttpMethod::POST, []);
$router = new Router($request);
$router->get('/test', $this->handler);

$response = $router->handleRequest();

expect($response->getStatus())->toBe(404);
expect($response->getStatus())->toBe(405);
});

it('should inject route parameters into the request object', function () {
Expand Down