Removed scoping from router (will be replaced by something else).
This commit is contained in:
parent
42f46cca78
commit
ee540d8137
5 changed files with 278 additions and 411 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2502.282232
|
||||
0.2502.282243
|
||||
|
|
|
@ -1,298 +0,0 @@
|
|||
<?php
|
||||
// HttpRouter.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-02-28
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
use stdClass;
|
||||
use InvalidArgumentException;
|
||||
use Index\Http\{
|
||||
HtmlHttpErrorHandler,HttpContentHandler,HttpErrorHandler,HttpResponse,
|
||||
HttpResponseBuilder,HttpRequest,HttpStream,PlainHttpErrorHandler,StringHttpContent
|
||||
};
|
||||
use Psr\Http\Message\{ResponseInterface,ServerRequestInterface};
|
||||
|
||||
class HttpRouter implements Router {
|
||||
/** @var array{handler: callable, match?: string, prefix?: string}[] */
|
||||
private array $middlewares = [];
|
||||
|
||||
/** @var array<string, array<string, callable>> */
|
||||
private array $staticRoutes = [];
|
||||
|
||||
/** @var array<string, array<string, callable>> */
|
||||
private array $dynamicRoutes = [];
|
||||
|
||||
private string $charSetValue;
|
||||
|
||||
/**
|
||||
* @param string $charSet Default character set to specify when none is present.
|
||||
* @param HttpErrorHandler|string $errorHandler Error handling to use for error responses with an empty body. 'html' for the default HTML implementation, 'plain' for the plaintext implementation.
|
||||
*/
|
||||
public function __construct(
|
||||
string $charSet = '',
|
||||
HttpErrorHandler|string $errorHandler = 'html'
|
||||
) {
|
||||
$this->charSetValue = $charSet;
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the normalised name of the preferred character set.
|
||||
*
|
||||
* @return string Normalised character set name.
|
||||
*/
|
||||
public string $charSet {
|
||||
get {
|
||||
if($this->charSetValue === '') {
|
||||
$charSet = mb_preferred_mime_name(mb_internal_encoding());
|
||||
if($charSet === false)
|
||||
$charSet = 'UTF-8';
|
||||
|
||||
return strtolower($charSet);
|
||||
}
|
||||
|
||||
return $this->charSetValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handler instance.
|
||||
*
|
||||
* @var HttpErrorHandler
|
||||
*/
|
||||
public HttpErrorHandler $errorHandler {
|
||||
get => $this->errorHandler;
|
||||
set(HttpErrorHandler|string $handler) {
|
||||
if($handler instanceof HttpErrorHandler)
|
||||
$this->errorHandler = $handler;
|
||||
elseif($handler === 'html')
|
||||
$this->setHtmlErrorHandler();
|
||||
else // plain
|
||||
$this->setPlainErrorHandler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error handler to the basic HTML one.
|
||||
*/
|
||||
public function setHtmlErrorHandler(): void {
|
||||
$this->errorHandler = new HtmlHttpErrorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error handler to the plain text one.
|
||||
*/
|
||||
public function setPlainErrorHandler(): void {
|
||||
$this->errorHandler = new PlainHttpErrorHandler;
|
||||
}
|
||||
|
||||
public function scopeTo(string $prefix): Router {
|
||||
return new ScopedRouter($this, $prefix);
|
||||
}
|
||||
|
||||
private static function preparePath(string $path, bool $prefixMatch): string|false {
|
||||
// this sucks lol
|
||||
if(!str_contains($path, '(') || !str_contains($path, ')'))
|
||||
return false;
|
||||
|
||||
// make trailing slash optional
|
||||
if(!$prefixMatch && str_ends_with($path, '/'))
|
||||
$path .= '?';
|
||||
|
||||
return sprintf('#^%s%s#su', $path, $prefixMatch ? '' : '$');
|
||||
}
|
||||
|
||||
public function use(string $path, callable $handler): void {
|
||||
$mwInfo = [];
|
||||
$mwInfo['handler'] = $handler;
|
||||
|
||||
$prepared = self::preparePath($path, true);
|
||||
if($prepared === false) {
|
||||
if(str_ends_with($path, '/'))
|
||||
$path = substr($path, 0, -1);
|
||||
|
||||
$mwInfo['prefix'] = $path;
|
||||
} else
|
||||
$mwInfo['match'] = $prepared;
|
||||
|
||||
$this->middlewares[] = $mwInfo;
|
||||
}
|
||||
|
||||
public function add(string $method, string $path, callable $handler): void {
|
||||
if($method === '')
|
||||
throw new InvalidArgumentException('$method may not be empty');
|
||||
|
||||
$method = strtoupper($method);
|
||||
if(trim($method) !== $method)
|
||||
throw new InvalidArgumentException('$method may start or end with whitespace');
|
||||
|
||||
$prepared = self::preparePath($path, false);
|
||||
if($prepared === false) {
|
||||
if(str_ends_with($path, '/'))
|
||||
$path = substr($path, 0, -1);
|
||||
|
||||
if(array_key_exists($path, $this->staticRoutes))
|
||||
$this->staticRoutes[$path][$method] = $handler;
|
||||
else
|
||||
$this->staticRoutes[$path] = [$method => $handler];
|
||||
} else {
|
||||
if(array_key_exists($prepared, $this->dynamicRoutes))
|
||||
$this->dynamicRoutes[$prepared][$method] = $handler;
|
||||
else
|
||||
$this->dynamicRoutes[$prepared] = [$method => $handler];
|
||||
}
|
||||
}
|
||||
|
||||
public function register(RouteHandler $handler): void {
|
||||
$handler->registerRoutes($this);
|
||||
}
|
||||
|
||||
public function resolve(string $method, string $path): ResolvedRouteInfo {
|
||||
if(str_ends_with($path, '/'))
|
||||
$path = substr($path, 0, -1);
|
||||
|
||||
$middlewares = [];
|
||||
|
||||
foreach($this->middlewares as $mwInfo) {
|
||||
if(array_key_exists('match', $mwInfo)) {
|
||||
if(preg_match($mwInfo['match'], $path, $args) !== 1)
|
||||
continue;
|
||||
|
||||
array_shift($args);
|
||||
} elseif(array_key_exists('prefix', $mwInfo)) {
|
||||
if($mwInfo['prefix'] !== '' && !str_starts_with($path, $mwInfo['prefix']))
|
||||
continue;
|
||||
|
||||
$args = [];
|
||||
} else continue;
|
||||
|
||||
$middlewares[] = [$mwInfo['handler'], $args];
|
||||
}
|
||||
|
||||
$methods = [];
|
||||
|
||||
if(array_key_exists($path, $this->staticRoutes)) {
|
||||
foreach($this->staticRoutes[$path] as $sMethodName => $sMethodHandler)
|
||||
$methods[$sMethodName] = [$sMethodHandler, []];
|
||||
} else {
|
||||
foreach($this->dynamicRoutes as $rPattern => $rMethods)
|
||||
if(preg_match($rPattern, $path, $args) === 1)
|
||||
foreach($rMethods as $rMethodName => $rMethodHandler)
|
||||
if(!array_key_exists($rMethodName, $methods))
|
||||
$methods[$rMethodName] = [$rMethodHandler, array_slice($args, 1)];
|
||||
}
|
||||
|
||||
$method = strtoupper($method);
|
||||
if(array_key_exists($method, $methods)) {
|
||||
[$handler, $args] = $methods[$method];
|
||||
} elseif($method === 'HEAD' && array_key_exists('GET', $methods)) {
|
||||
[$handler, $args] = $methods['GET'];
|
||||
} else {
|
||||
$handler = null;
|
||||
$args = [];
|
||||
}
|
||||
|
||||
return new ResolvedRouteInfo($middlewares, array_keys($methods), $handler, $args);
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface {
|
||||
$response = new HttpResponseBuilder;
|
||||
$args = [$response, $request];
|
||||
|
||||
$routeInfo = $this->resolve($request->getMethod(), $request->getUri()->getPath());
|
||||
|
||||
// always run middleware regardless of 404 or 405
|
||||
$result = $routeInfo->runMiddleware($args);
|
||||
if($result === null) {
|
||||
if(!$routeInfo->hasHandler()) {
|
||||
if(empty($routeInfo->supportedMethods)) {
|
||||
$result = 404;
|
||||
} else {
|
||||
$result = 405;
|
||||
$response->setHeader('Allow', implode(', ', $routeInfo->supportedMethods));
|
||||
}
|
||||
} else
|
||||
$result = $routeInfo->dispatch($args);
|
||||
}
|
||||
|
||||
if(is_int($result)) {
|
||||
if($result >= 100 && $result < 600)
|
||||
$this->writeErrorPage($response, $request, $result);
|
||||
} elseif(empty($response->content)) {
|
||||
if(is_scalar($result)) {
|
||||
$result = (string)$result;
|
||||
$response->body = HttpStream::createStream($result);
|
||||
|
||||
if(!$response->hasContentType()) {
|
||||
if(strtolower(substr($result, 0, 14)) === '<!doctype html')
|
||||
$response->setTypeHtml($this->charSet);
|
||||
else {
|
||||
$charset = mb_detect_encoding($result);
|
||||
if($charset !== false)
|
||||
$charset = mb_preferred_mime_name($charset);
|
||||
$charset = $charset === false ? 'utf-8' : strtolower($charset);
|
||||
|
||||
if(strtolower(substr($result, 0, 5)) === '<?xml')
|
||||
$response->setTypeXml($charset);
|
||||
else
|
||||
$response->setTypePlain($charset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $response->toResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a route based on a given HTTP request message with additional prefix arguments and output to stdout.
|
||||
*
|
||||
* @param ?HttpRequest $request HTTP request message to handle, null to use the current request.
|
||||
*/
|
||||
public function dispatch(?HttpRequest $request = null): void {
|
||||
$request ??= HttpRequest::fromRequest();
|
||||
self::output($this->handle($request), $request->method !== 'HEAD');
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an error page to a given HTTP response builder.
|
||||
*
|
||||
* @param HttpResponseBuilder $response HTTP response builder to apply the error page to.
|
||||
* @param ServerRequestInterface $request HTTP request that triggered this error.
|
||||
* @param int $statusCode HTTP status code for this error page.
|
||||
*/
|
||||
public function writeErrorPage(HttpResponseBuilder $response, ServerRequestInterface $request, int $statusCode): void {
|
||||
$this->errorHandler->handle(
|
||||
$response,
|
||||
$request,
|
||||
$response->statusCode = $statusCode,
|
||||
$response->reasonPhrase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a HTTP response message to stdout.
|
||||
*
|
||||
* @param ResponseInterface $response HTTP response message to output.
|
||||
* @param bool $includeBody true to include the response message body, false to omit it for HEAD requests.
|
||||
*/
|
||||
public static function output(ResponseInterface $response, bool $includeBody): void {
|
||||
header(sprintf(
|
||||
'HTTP/%s %03d %s',
|
||||
$response->getProtocolVersion(),
|
||||
$response->getStatusCode(),
|
||||
$response->getReasonPhrase(),
|
||||
));
|
||||
|
||||
foreach($response->getHeaders() as $name => $lines)
|
||||
foreach($lines as $line)
|
||||
header(sprintf('%s: %s', $name, $line));
|
||||
|
||||
if($includeBody) {
|
||||
$stream = $response->getBody();
|
||||
if($stream->isReadable())
|
||||
echo (string)$stream;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,55 +5,291 @@
|
|||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
use stdClass;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Index\Http\HttpRequest;
|
||||
use Index\Http\{
|
||||
HtmlHttpErrorHandler,HttpContentHandler,HttpErrorHandler,HttpResponse,
|
||||
HttpResponseBuilder,HttpRequest,HttpStream,PlainHttpErrorHandler,StringHttpContent
|
||||
};
|
||||
use Psr\Http\Message\{ResponseInterface,ServerRequestInterface};
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface for defining HTTP routers.
|
||||
*/
|
||||
interface Router extends RequestHandlerInterface {
|
||||
/**
|
||||
* Retrieve a scoped router to a given path prefix.
|
||||
*
|
||||
* @param string $prefix Prefix to apply to paths within the returned router.
|
||||
* @return Router Scoped router proxy.
|
||||
*/
|
||||
public function scopeTo(string $prefix): Router;
|
||||
class Router implements RequestHandlerInterface {
|
||||
/** @var array{handler: callable, match?: string, prefix?: string}[] */
|
||||
private array $middlewares = [];
|
||||
|
||||
/** @var array<string, array<string, callable>> */
|
||||
private array $staticRoutes = [];
|
||||
|
||||
/** @var array<string, array<string, callable>> */
|
||||
private array $dynamicRoutes = [];
|
||||
|
||||
private string $charSetValue;
|
||||
|
||||
/**
|
||||
* Registers a middleware handler.
|
||||
*
|
||||
* @param string $path Path prefix or regex to apply this middleware on.
|
||||
* @param callable $handler Middleware handler.
|
||||
* @param string $charSet Default character set to specify when none is present.
|
||||
* @param HttpErrorHandler|string $errorHandler Error handling to use for error responses with an empty body. 'html' for the default HTML implementation, 'plain' for the plaintext implementation.
|
||||
*/
|
||||
public function use(string $path, callable $handler): void;
|
||||
public function __construct(
|
||||
string $charSet = '',
|
||||
HttpErrorHandler|string $errorHandler = 'html'
|
||||
) {
|
||||
$this->charSetValue = $charSet;
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a route handler for a given method and path.
|
||||
* Retrieves the normalised name of the preferred character set.
|
||||
*
|
||||
* @param string $method Method to use this handler for.
|
||||
* @param string $path Path or regex to use this handler with.
|
||||
* @param callable $handler Handler to use for this method/path combination.
|
||||
* @throws InvalidArgumentException If $method is empty.
|
||||
* @throws InvalidArgumentException If $method starts or ends with spaces.
|
||||
* @return string Normalised character set name.
|
||||
*/
|
||||
public function add(string $method, string $path, callable $handler): void;
|
||||
public string $charSet {
|
||||
get {
|
||||
if($this->charSetValue === '') {
|
||||
$charSet = mb_preferred_mime_name(mb_internal_encoding());
|
||||
if($charSet === false)
|
||||
$charSet = 'UTF-8';
|
||||
|
||||
return strtolower($charSet);
|
||||
}
|
||||
|
||||
return $this->charSetValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves middlewares and a route handler for a given method and path.
|
||||
* Error handler instance.
|
||||
*
|
||||
* @param string $method Method to resolve for.
|
||||
* @param string $path Path to resolve for.
|
||||
* @return ResolvedRouteInfo Resolved route information.
|
||||
* @var HttpErrorHandler
|
||||
*/
|
||||
public function resolve(string $method, string $path): ResolvedRouteInfo;
|
||||
public HttpErrorHandler $errorHandler {
|
||||
get => $this->errorHandler;
|
||||
set(HttpErrorHandler|string $handler) {
|
||||
if($handler instanceof HttpErrorHandler)
|
||||
$this->errorHandler = $handler;
|
||||
elseif($handler === 'html')
|
||||
$this->setHtmlErrorHandler();
|
||||
else // plain
|
||||
$this->setPlainErrorHandler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers routes in an RouteHandler implementation.
|
||||
*
|
||||
* @param RouteHandler $handler Routes handler.
|
||||
* Set the error handler to the basic HTML one.
|
||||
*/
|
||||
public function register(RouteHandler $handler): void;
|
||||
public function setHtmlErrorHandler(): void {
|
||||
$this->errorHandler = new HtmlHttpErrorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error handler to the plain text one.
|
||||
*/
|
||||
public function setPlainErrorHandler(): void {
|
||||
$this->errorHandler = new PlainHttpErrorHandler;
|
||||
}
|
||||
|
||||
private static function preparePath(string $path, bool $prefixMatch): string|false {
|
||||
// this sucks lol
|
||||
if(!str_contains($path, '(') || !str_contains($path, ')'))
|
||||
return false;
|
||||
|
||||
// make trailing slash optional
|
||||
if(!$prefixMatch && str_ends_with($path, '/'))
|
||||
$path .= '?';
|
||||
|
||||
return sprintf('#^%s%s#su', $path, $prefixMatch ? '' : '$');
|
||||
}
|
||||
|
||||
public function use(string $path, callable $handler): void {
|
||||
$mwInfo = [];
|
||||
$mwInfo['handler'] = $handler;
|
||||
|
||||
$prepared = self::preparePath($path, true);
|
||||
if($prepared === false) {
|
||||
if(str_ends_with($path, '/'))
|
||||
$path = substr($path, 0, -1);
|
||||
|
||||
$mwInfo['prefix'] = $path;
|
||||
} else
|
||||
$mwInfo['match'] = $prepared;
|
||||
|
||||
$this->middlewares[] = $mwInfo;
|
||||
}
|
||||
|
||||
public function add(string $method, string $path, callable $handler): void {
|
||||
if($method === '')
|
||||
throw new InvalidArgumentException('$method may not be empty');
|
||||
|
||||
$method = strtoupper($method);
|
||||
if(trim($method) !== $method)
|
||||
throw new InvalidArgumentException('$method may start or end with whitespace');
|
||||
|
||||
$prepared = self::preparePath($path, false);
|
||||
if($prepared === false) {
|
||||
if(str_ends_with($path, '/'))
|
||||
$path = substr($path, 0, -1);
|
||||
|
||||
if(array_key_exists($path, $this->staticRoutes))
|
||||
$this->staticRoutes[$path][$method] = $handler;
|
||||
else
|
||||
$this->staticRoutes[$path] = [$method => $handler];
|
||||
} else {
|
||||
if(array_key_exists($prepared, $this->dynamicRoutes))
|
||||
$this->dynamicRoutes[$prepared][$method] = $handler;
|
||||
else
|
||||
$this->dynamicRoutes[$prepared] = [$method => $handler];
|
||||
}
|
||||
}
|
||||
|
||||
public function register(RouteHandler $handler): void {
|
||||
$handler->registerRoutes($this);
|
||||
}
|
||||
|
||||
public function resolve(string $method, string $path): ResolvedRouteInfo {
|
||||
if(str_ends_with($path, '/'))
|
||||
$path = substr($path, 0, -1);
|
||||
|
||||
$middlewares = [];
|
||||
|
||||
foreach($this->middlewares as $mwInfo) {
|
||||
if(array_key_exists('match', $mwInfo)) {
|
||||
if(preg_match($mwInfo['match'], $path, $args) !== 1)
|
||||
continue;
|
||||
|
||||
array_shift($args);
|
||||
} elseif(array_key_exists('prefix', $mwInfo)) {
|
||||
if($mwInfo['prefix'] !== '' && !str_starts_with($path, $mwInfo['prefix']))
|
||||
continue;
|
||||
|
||||
$args = [];
|
||||
} else continue;
|
||||
|
||||
$middlewares[] = [$mwInfo['handler'], $args];
|
||||
}
|
||||
|
||||
$methods = [];
|
||||
|
||||
if(array_key_exists($path, $this->staticRoutes)) {
|
||||
foreach($this->staticRoutes[$path] as $sMethodName => $sMethodHandler)
|
||||
$methods[$sMethodName] = [$sMethodHandler, []];
|
||||
} else {
|
||||
foreach($this->dynamicRoutes as $rPattern => $rMethods)
|
||||
if(preg_match($rPattern, $path, $args) === 1)
|
||||
foreach($rMethods as $rMethodName => $rMethodHandler)
|
||||
if(!array_key_exists($rMethodName, $methods))
|
||||
$methods[$rMethodName] = [$rMethodHandler, array_slice($args, 1)];
|
||||
}
|
||||
|
||||
$method = strtoupper($method);
|
||||
if(array_key_exists($method, $methods)) {
|
||||
[$handler, $args] = $methods[$method];
|
||||
} elseif($method === 'HEAD' && array_key_exists('GET', $methods)) {
|
||||
[$handler, $args] = $methods['GET'];
|
||||
} else {
|
||||
$handler = null;
|
||||
$args = [];
|
||||
}
|
||||
|
||||
return new ResolvedRouteInfo($middlewares, array_keys($methods), $handler, $args);
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface {
|
||||
$response = new HttpResponseBuilder;
|
||||
$args = [$response, $request];
|
||||
|
||||
$routeInfo = $this->resolve($request->getMethod(), $request->getUri()->getPath());
|
||||
|
||||
// always run middleware regardless of 404 or 405
|
||||
$result = $routeInfo->runMiddleware($args);
|
||||
if($result === null) {
|
||||
if(!$routeInfo->hasHandler()) {
|
||||
if(empty($routeInfo->supportedMethods)) {
|
||||
$result = 404;
|
||||
} else {
|
||||
$result = 405;
|
||||
$response->setHeader('Allow', implode(', ', $routeInfo->supportedMethods));
|
||||
}
|
||||
} else
|
||||
$result = $routeInfo->dispatch($args);
|
||||
}
|
||||
|
||||
if(is_int($result)) {
|
||||
if($result >= 100 && $result < 600)
|
||||
$this->writeErrorPage($response, $request, $result);
|
||||
} elseif(empty($response->content)) {
|
||||
if(is_scalar($result)) {
|
||||
$result = (string)$result;
|
||||
$response->body = HttpStream::createStream($result);
|
||||
|
||||
if(!$response->hasContentType()) {
|
||||
if(strtolower(substr($result, 0, 14)) === '<!doctype html')
|
||||
$response->setTypeHtml($this->charSet);
|
||||
else {
|
||||
$charset = mb_detect_encoding($result);
|
||||
if($charset !== false)
|
||||
$charset = mb_preferred_mime_name($charset);
|
||||
$charset = $charset === false ? 'utf-8' : strtolower($charset);
|
||||
|
||||
if(strtolower(substr($result, 0, 5)) === '<?xml')
|
||||
$response->setTypeXml($charset);
|
||||
else
|
||||
$response->setTypePlain($charset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $response->toResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a route based on a given HTTP request message with additional prefix arguments and output to stdout.
|
||||
*
|
||||
* @param ?HttpRequest $request HTTP request message to handle, null to use the current request.
|
||||
*/
|
||||
public function dispatch(?HttpRequest $request = null): void {
|
||||
$request ??= HttpRequest::fromRequest();
|
||||
self::output($this->handle($request), $request->method !== 'HEAD');
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an error page to a given HTTP response builder.
|
||||
*
|
||||
* @param HttpResponseBuilder $response HTTP response builder to apply the error page to.
|
||||
* @param ServerRequestInterface $request HTTP request that triggered this error.
|
||||
* @param int $statusCode HTTP status code for this error page.
|
||||
*/
|
||||
public function writeErrorPage(HttpResponseBuilder $response, ServerRequestInterface $request, int $statusCode): void {
|
||||
$this->errorHandler->handle(
|
||||
$response,
|
||||
$request,
|
||||
$response->statusCode = $statusCode,
|
||||
$response->reasonPhrase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a HTTP response message to stdout.
|
||||
*
|
||||
* @param ResponseInterface $response HTTP response message to output.
|
||||
* @param bool $includeBody true to include the response message body, false to omit it for HEAD requests.
|
||||
*/
|
||||
public static function output(ResponseInterface $response, bool $includeBody): void {
|
||||
header(sprintf(
|
||||
'HTTP/%s %03d %s',
|
||||
$response->getProtocolVersion(),
|
||||
$response->getStatusCode(),
|
||||
$response->getReasonPhrase(),
|
||||
));
|
||||
|
||||
foreach($response->getHeaders() as $name => $lines)
|
||||
foreach($lines as $line)
|
||||
header(sprintf('%s: %s', $name, $line));
|
||||
|
||||
if($includeBody) {
|
||||
$stream = $response->getBody();
|
||||
if($stream->isReadable())
|
||||
echo (string)$stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
// ScopedRouter.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-02-28
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
use Psr\Http\Message\{ResponseInterface,ServerRequestInterface};
|
||||
|
||||
/**
|
||||
* Provides a scoped router interface, automatically adds a prefix to any routes added.
|
||||
*/
|
||||
class ScopedRouter implements Router {
|
||||
/**
|
||||
* @param Router $router Underlying router.
|
||||
* @param string $prefix Base path to use as a prefix.
|
||||
*/
|
||||
public function __construct(
|
||||
private Router $router,
|
||||
private string $prefix
|
||||
) {
|
||||
if($router instanceof ScopedRouter)
|
||||
$this->router = $router->router;
|
||||
|
||||
// TODO: cleanup prefix
|
||||
}
|
||||
|
||||
public function scopeTo(string $prefix): Router {
|
||||
return $this->router->scopeTo($this->prefix . $prefix);
|
||||
}
|
||||
|
||||
public function use(string $path, callable $handler): void {
|
||||
$this->router->use($this->prefix . $path, $handler);
|
||||
}
|
||||
|
||||
public function add(string $method, string $path, callable $handler): void {
|
||||
$this->router->add($method, $this->prefix . $path, $handler);
|
||||
}
|
||||
|
||||
public function register(RouteHandler $handler): void {
|
||||
$handler->registerRoutes($this);
|
||||
}
|
||||
|
||||
public function resolve(string $method, string $path): ResolvedRouteInfo {
|
||||
return $this->router->resolve($method, $this->prefix . $path);
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface {
|
||||
return $this->router->handle($request->withRequestTarget($this->prefix . $request->getRequestTarget()));
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ declare(strict_types=1);
|
|||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Index\Http\{HttpHeaders,HttpRequest,HttpUri,NullStream};
|
||||
use Index\Http\Routing\{HttpMiddleware,HttpRoute,HttpRouter,RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\{HttpMiddleware,HttpRoute,Router,RouteHandler,RouteHandlerCommon};
|
||||
|
||||
/**
|
||||
* This test isn't super representative of the current functionality
|
||||
|
@ -16,12 +16,12 @@ use Index\Http\Routing\{HttpMiddleware,HttpRoute,HttpRouter,RouteHandler,RouteHa
|
|||
*/
|
||||
#[CoversClass(HttpMiddleware::class)]
|
||||
#[CoversClass(HttpRoute::class)]
|
||||
#[CoversClass(HttpRouter::class)]
|
||||
#[CoversClass(Router::class)]
|
||||
#[CoversClass(RouteHandler::class)]
|
||||
#[CoversClass(RouteHandlerCommon::class)]
|
||||
final class RouterTest extends TestCase {
|
||||
public function testRouter(): void {
|
||||
$router1 = new HttpRouter;
|
||||
$router1 = new Router;
|
||||
|
||||
$router1->add('GET', '/', fn() => 'get');
|
||||
$router1->add('POST', '/', fn() => 'post');
|
||||
|
@ -57,7 +57,7 @@ final class RouterTest extends TestCase {
|
|||
$this->assertEquals('warioware below flashwave', $getWariowareBelowFlashwave->runMiddleware([]));
|
||||
$this->assertEquals('below flashwave', $getWariowareBelowFlashwave->dispatch([]));
|
||||
|
||||
$router2 = new HttpRouter;
|
||||
$router2 = new Router;
|
||||
$router2->use('/', fn() => 'meow');
|
||||
$router2->add('GET', '/rules', fn() => 'rules page');
|
||||
$router2->add('GET', '/contact', fn() => 'contact page');
|
||||
|
@ -70,21 +70,10 @@ final class RouterTest extends TestCase {
|
|||
$get25252 = $router2->resolve('GET', '/25252');
|
||||
$this->assertEquals('meow', $get25252->runMiddleware([]));
|
||||
$this->assertEquals('numeric test', $get25252->dispatch([]));
|
||||
|
||||
$router3 = $router1->scopeTo('/scoped');
|
||||
$router3->add('GET', '/static', fn() => 'wrong');
|
||||
$router1->add('GET', '/scoped/static/0', fn() => 'correct');
|
||||
$router3->add('GET', '/variable', fn() => 'wrong');
|
||||
$router3->add('GET', '/variable/([0-9]+)', fn(string $num) => $num === '0' ? 'correct' : 'VERY wrong');
|
||||
$router3->add('GET', '/variable/([a-z]+)', fn(string $char) => $char === 'a' ? 'correct' : 'VERY wrong');
|
||||
|
||||
$this->assertEquals('correct', $router3->resolve('GET', '/static/0')->dispatch([]));
|
||||
$this->assertEquals('correct', $router1->resolve('GET', '/scoped/variable/0')->dispatch([]));
|
||||
$this->assertEquals('correct', $router3->resolve('GET', '/variable/a')->dispatch([]));
|
||||
}
|
||||
|
||||
public function testAttribute(): void {
|
||||
$router = new HttpRouter;
|
||||
$router = new Router;
|
||||
$handler = new class implements RouteHandler {
|
||||
use RouteHandlerCommon;
|
||||
|
||||
|
@ -142,19 +131,10 @@ final class RouterTest extends TestCase {
|
|||
$getMw = $router->resolve('GET', '/mw');
|
||||
$this->assertEquals('this intercepts', $getMw->runMiddleware([]));
|
||||
$this->assertEquals('this is intercepted', $getMw->dispatch([]));
|
||||
|
||||
$scoped = $router->scopeTo('/scoped');
|
||||
$scoped->register($handler);
|
||||
|
||||
$this->assertEquals('index', $scoped->resolve('GET', '/')->dispatch([]));
|
||||
$this->assertEquals('avatar', $router->resolve('POST', '/scoped/avatar')->dispatch([]));
|
||||
$this->assertEquals('static', $scoped->resolve('PUT', '/static')->dispatch([]));
|
||||
$this->assertEquals('meow', $router->resolve('GET', '/scoped/meow')->dispatch([]));
|
||||
$this->assertEquals('meow', $scoped->resolve('POST', '/meow')->dispatch([]));
|
||||
}
|
||||
|
||||
public function testEEPROMSituation(): void {
|
||||
$router = new HttpRouter;
|
||||
$router = new Router;
|
||||
|
||||
$router->add('OPTIONS', '/uploads/([A-Za-z0-9\-_]+)(?:\.(t|json))?', function() {});
|
||||
$router->add('GET', '/uploads/([A-Za-z0-9\-_]+)(?:\.(t|json))?', function() {});
|
||||
|
@ -166,7 +146,7 @@ final class RouterTest extends TestCase {
|
|||
}
|
||||
|
||||
public function testMiddlewareInterceptionOnRoot(): void {
|
||||
$router = new HttpRouter;
|
||||
$router = new Router;
|
||||
$router->use('/', fn() => 'expected');
|
||||
$router->add('GET', '/', fn() => 'unexpected');
|
||||
$router->add('GET', '/test', fn() => 'also unexpected');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue