Redid base route pipeline.
This commit is contained in:
parent
d0b9b2d556
commit
bc43662792
12 changed files with 372 additions and 238 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2503.70242
|
||||
0.2503.72000
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
<?php
|
||||
// HtmlHttpErrorHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-02-28
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Represents a basic HTML error message handler for building HTTP response messages.
|
||||
*/
|
||||
class HtmlHttpErrorHandler implements HttpErrorHandler {
|
||||
private const TEMPLATE = <<<HTML
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=":charset">
|
||||
<title>:code :message</title>
|
||||
</head>
|
||||
<body>
|
||||
<center><h1>:code :message</h1><center>
|
||||
<hr>
|
||||
<center>Index</center>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
|
||||
public function handle(HttpResponseBuilder $response, ServerRequestInterface $request, int $code, string $message): void {
|
||||
$response->setTypeHtml();
|
||||
|
||||
$charSet = mb_preferred_mime_name(mb_internal_encoding());
|
||||
if($charSet === false)
|
||||
$charSet = 'UTF-8';
|
||||
|
||||
$response->body = HttpStream::createStream(strtr(self::TEMPLATE, [
|
||||
':charset' => strtolower($charSet),
|
||||
':code' => sprintf('%03d', $code),
|
||||
':message' => $message,
|
||||
]));
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
// HttpErrorHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-02-28
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Represents an error message handler for building HTTP response messages.
|
||||
*/
|
||||
interface HttpErrorHandler {
|
||||
/**
|
||||
* Applies an error message template to the provided HTTP response builder.
|
||||
*
|
||||
* @param HttpResponseBuilder $response HTTP Response builder to apply this error to.
|
||||
* @param ServerRequestInterface $request HTTP Request this error is a response to.
|
||||
* @param int $code HTTP status code to apply.
|
||||
* @param string $message HTTP status message to apply.
|
||||
*/
|
||||
public function handle(HttpResponseBuilder $response, ServerRequestInterface $request, int $code, string $message): void;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpRequest.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2025-03-02
|
||||
// Updated: 2025-03-07
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -20,6 +20,9 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
/** Name of attribute containing the remote address value. */
|
||||
public const string ATTR_REMOTE_ADDRESS = 'remoteAddress';
|
||||
|
||||
/** HTTP request method. */
|
||||
public private(set) string $method;
|
||||
|
||||
/**
|
||||
* @param string $protocolVersion HTTP message version.
|
||||
* @param array<string, string[]> $headers HTTP message headers.
|
||||
|
@ -37,7 +40,7 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
array $headers,
|
||||
StreamInterface $body,
|
||||
public private(set) array $attributes,
|
||||
public private(set) string $method,
|
||||
string $method,
|
||||
public private(set) HttpUri $uri,
|
||||
public private(set) array $params,
|
||||
public private(set) array $cookies,
|
||||
|
@ -61,6 +64,8 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
if(filter_var($attributes[self::ATTR_REMOTE_ADDRESS], FILTER_VALIDATE_IP) === false)
|
||||
throw new InvalidArgumentException('remoteAddress attribute must be a valid remote address');
|
||||
}
|
||||
|
||||
$this->method = strtoupper($method);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -328,7 +333,7 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
* @return null|array<string, string[]|object[]>|object The deserialized body parameters, if any. These will typically be an array or object.
|
||||
*/
|
||||
public function getParsedBody() {
|
||||
// this contains a couple hard coded body parsers but you should really be using middleware
|
||||
// this contains a couple hard coded body parsers but you should really be using preprocessors
|
||||
if($this->parsedBody === null) {
|
||||
$this->parsedBody = [];
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpResponse.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2025-02-28
|
||||
// Updated: 2025-03-07
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -39,8 +39,7 @@ class HttpResponse extends HttpMessage implements ResponseInterface {
|
|||
if($statusCode < 100 || $statusCode > 599)
|
||||
throw new InvalidArgumentException('$statusCode is not an acceptable value, it must be equal to or greater than 100 and less than 600');
|
||||
|
||||
$this->reasonPhrase = $reasonPhrase === '' && array_key_exists($statusCode, self::REASON_PHRASES)
|
||||
? self::REASON_PHRASES[$statusCode] : $reasonPhrase;
|
||||
$this->reasonPhrase = $reasonPhrase === '' ? self::defaultReasonPhase($statusCode) : $reasonPhrase;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,6 +116,17 @@ class HttpResponse extends HttpMessage implements ResponseInterface {
|
|||
return $this->with(statusCode: $code, reasonPhrase: $reasonPhrase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default reason phrase for a given status code.
|
||||
*
|
||||
* @param int $statusCode HTTP status code.
|
||||
* @param string $fallback Fallback message, if not match was found.
|
||||
* @return string
|
||||
*/
|
||||
public static function defaultReasonPhase(int $statusCode, string $fallback = 'Unknown Status'): string {
|
||||
return array_key_exists($statusCode, self::REASON_PHRASES) ? self::REASON_PHRASES[$statusCode] : $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default HTTP reason phrases.
|
||||
*
|
||||
|
|
|
@ -26,6 +26,18 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
*/
|
||||
public string $reasonPhrase = '';
|
||||
|
||||
/**
|
||||
* If true, a body still needs to be set.
|
||||
*/
|
||||
public bool $needsBody {
|
||||
get => $this->body === null
|
||||
&& $this->statusCode !== 204 && $this->statusCode !== 205
|
||||
&& (
|
||||
($this->statusCode >= 200 && $this->statusCode <= 299)
|
||||
|| ($this->statusCode >= 400 && $this->statusCode <= 599)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a cookie to this response.
|
||||
*
|
||||
|
@ -120,7 +132,7 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets an X-Powered-By header.
|
||||
* Sets the X-Powered-By header.
|
||||
*
|
||||
* @param string $poweredBy Thing that your website is powered by.
|
||||
*/
|
||||
|
@ -129,7 +141,34 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets an ETag header.
|
||||
* Sets the Allow header for which HTTP methods are allowed.
|
||||
*
|
||||
* @param string[] $methods Allowed methods.
|
||||
*/
|
||||
public function setAllow(array $methods): void {
|
||||
$this->setHeader('Allow', implode(', ', $methods));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Content-Length header is present.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasContentLength(): bool {
|
||||
return $this->hasHeader('Content-Length');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content-Length header.
|
||||
*
|
||||
* @param int $bytes Length of the body in bytes.
|
||||
*/
|
||||
public function setContentLength(int $bytes): void {
|
||||
$this->setHeader('Content-Length', (string)max(0, $bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ETag header.
|
||||
*
|
||||
* @param string $eTag Unique identifier for the current state of the content.
|
||||
* @param bool $weak Whether to use weak matching.
|
||||
|
@ -144,7 +183,7 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets a Server-Timing header.
|
||||
* Sets the Server-Timing header.
|
||||
*
|
||||
* @param Timings $timings Timings to supply to the devtools.
|
||||
*/
|
||||
|
@ -174,7 +213,7 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets a Content-Type header.
|
||||
* Sets the Content-Type header.
|
||||
*
|
||||
* @param MediaType|string $mediaType Media type to set as the content type of the response body.
|
||||
*/
|
||||
|
@ -194,8 +233,12 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypePlain(string $charset = 'us-ascii'): void {
|
||||
$this->setContentType('text/plain; charset=' . $charset);
|
||||
public function setTypePlain(string $charset = ''): void {
|
||||
$type = 'text/plain';
|
||||
if($charset !== '')
|
||||
$type .= sprintf(';charset=%s', $charset);
|
||||
|
||||
$this->setContentType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -203,8 +246,12 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypeHtml(string $charset = 'utf-8'): void {
|
||||
$this->setContentType('text/html; charset=' . $charset);
|
||||
public function setTypeHtml(string $charset = ''): void {
|
||||
$type = 'text/html';
|
||||
if($charset !== '')
|
||||
$type .= sprintf(';charset=%s', $charset);
|
||||
|
||||
$this->setContentType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -212,8 +259,19 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypeJson(string $charset = 'utf-8'): void {
|
||||
$this->setContentType('application/json; charset=' . $charset);
|
||||
public function setTypeJson(string $charset = ''): void {
|
||||
$type = 'application/json';
|
||||
if($charset !== '')
|
||||
$type .= sprintf(';charset=%s', $charset);
|
||||
|
||||
$this->setContentType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content-Type to 'application/x-bittorrent'.
|
||||
*/
|
||||
public function setTypeBencode(): void {
|
||||
$this->setContentType('application/x-bittorrent');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -221,8 +279,12 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypeXml(string $charset = 'utf-8'): void {
|
||||
$this->setContentType('application/xml; charset=' . $charset);
|
||||
public function setTypeXml(string $charset = ''): void {
|
||||
$type = 'application/xml';
|
||||
if($charset !== '')
|
||||
$type .= sprintf(';charset=%s', $charset);
|
||||
|
||||
$this->setContentType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -230,8 +292,12 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypeCss(string $charset = 'utf-8'): void {
|
||||
$this->setContentType('text/css; charset=' . $charset);
|
||||
public function setTypeCss(string $charset = ''): void {
|
||||
$type = 'text/css';
|
||||
if($charset !== '')
|
||||
$type .= sprintf(';charset=%s', $charset);
|
||||
|
||||
$this->setContentType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -239,12 +305,16 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypeJs(string $charset = 'utf-8'): void {
|
||||
$this->setContentType('application/javascript; charset=' . $charset);
|
||||
public function setTypeJs(string $charset = ''): void {
|
||||
$type = 'application/javascript';
|
||||
if($charset !== '')
|
||||
$type .= sprintf(';charset=%s', $charset);
|
||||
|
||||
$this->setContentType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies an Apache Web Server X-Sendfile header.
|
||||
* Specifies the Apache Web Server X-Sendfile header.
|
||||
*
|
||||
* @param string $absolutePath Absolute path to the content to serve.
|
||||
*/
|
||||
|
@ -253,7 +323,7 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Specifies an NGINX X-Accel-Redirect header.
|
||||
* Specifies the NGINX X-Accel-Redirect header.
|
||||
*
|
||||
* @param string $uri Relative URI to the content to serve.
|
||||
*/
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
// PlainHttpErrorHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-02-28
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Represents a plain text error message handler for building HTTP response messages.
|
||||
*/
|
||||
class PlainHttpErrorHandler implements HttpErrorHandler {
|
||||
public function handle(HttpResponseBuilder $response, ServerRequestInterface $request, int $code, string $message): void {
|
||||
$response->setTypePlain();
|
||||
$response->body = HttpStream::createStream(sprintf('HTTP %03d %s', $code, $message));
|
||||
}
|
||||
}
|
20
src/Http/Routing/ErrorHandling/ErrorHandler.php
Normal file
20
src/Http/Routing/ErrorHandling/ErrorHandler.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
// ErrorHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-03-07
|
||||
|
||||
namespace Index\Http\Routing\ErrorHandling;
|
||||
|
||||
use Index\Http\Routing\HandlerContext;
|
||||
|
||||
/**
|
||||
* Represents an error message handler for building HTTP response messages.
|
||||
*/
|
||||
interface ErrorHandler {
|
||||
/**
|
||||
* Applies an error message template to the provided HTTP response builder.
|
||||
*
|
||||
* @param HandlerContext $context Route context.
|
||||
*/
|
||||
public function handle(HandlerContext $context): void;
|
||||
}
|
48
src/Http/Routing/ErrorHandling/HtmlErrorHandler.php
Normal file
48
src/Http/Routing/ErrorHandling/HtmlErrorHandler.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
// HtmlErrorHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-03-07
|
||||
|
||||
namespace Index\Http\Routing\ErrorHandling;
|
||||
|
||||
use Index\Http\{HttpResponse,HttpStream};
|
||||
use Index\Http\Routing\HandlerContext;
|
||||
|
||||
/**
|
||||
* Represents a basic HTML error message handler for building HTTP response messages.
|
||||
*/
|
||||
class HtmlErrorHandler implements ErrorHandler {
|
||||
private const TEMPLATE = <<<HTML
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=":charset">
|
||||
<title>:code :message</title>
|
||||
</head>
|
||||
<body>
|
||||
<center><h1>:code :message</h1><center>
|
||||
<hr>
|
||||
<center>Index</center>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
|
||||
public function handle(HandlerContext $context): void {
|
||||
if(!$context->response->needsBody)
|
||||
return;
|
||||
|
||||
$context->response->setTypeHtml();
|
||||
|
||||
$charSet = mb_preferred_mime_name(mb_internal_encoding());
|
||||
if($charSet === false)
|
||||
$charSet = 'UTF-8';
|
||||
|
||||
$context->response->body = HttpStream::createStream(strtr(self::TEMPLATE, [
|
||||
':charset' => strtolower($charSet),
|
||||
':code' => sprintf('%03d', $context->response->statusCode),
|
||||
':message' => $context->response->reasonPhrase === ''
|
||||
? HttpResponse::defaultReasonPhase($context->response->statusCode)
|
||||
: $context->response->reasonPhrase,
|
||||
]));
|
||||
}
|
||||
}
|
28
src/Http/Routing/ErrorHandling/PlainErrorHandler.php
Normal file
28
src/Http/Routing/ErrorHandling/PlainErrorHandler.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
// PlainErrorHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-03-07
|
||||
|
||||
namespace Index\Http\Routing\ErrorHandling;
|
||||
|
||||
use Index\Http\{HttpResponse,HttpStream};
|
||||
use Index\Http\Routing\HandlerContext;
|
||||
|
||||
/**
|
||||
* Represents a plain text error message handler for building HTTP response messages.
|
||||
*/
|
||||
class PlainErrorHandler implements ErrorHandler {
|
||||
public function handle(HandlerContext $context): void {
|
||||
if(!$context->response->needsBody)
|
||||
return;
|
||||
|
||||
$context->response->setTypePlain();
|
||||
$context->response->body = HttpStream::createStream(sprintf(
|
||||
'HTTP %03d %s',
|
||||
$context->response->statusCode,
|
||||
$context->response->reasonPhrase === ''
|
||||
? HttpResponse::defaultReasonPhase($context->response->statusCode)
|
||||
: $context->response->reasonPhrase
|
||||
));
|
||||
}
|
||||
}
|
|
@ -12,15 +12,24 @@ use InvalidArgumentException;
|
|||
* Information of a route.
|
||||
*/
|
||||
class RouteInfo {
|
||||
/** HTTP method this route serves. */
|
||||
/**
|
||||
* HTTP method this route serves.
|
||||
*/
|
||||
public private(set) string $method;
|
||||
|
||||
/**
|
||||
* Names of middleware to run before calling the method handler. Executed in order of registration.
|
||||
* Names of preprocessors to run before calling the method handler. Executed in order of registration.
|
||||
*
|
||||
* @var array<string, array<int|string, mixed>>
|
||||
*/
|
||||
public private(set) array $middlewares = [];
|
||||
public private(set) array $preprocs = [];
|
||||
|
||||
/**
|
||||
* Names of postprocessors to run after calling the method handler. Executed in order of registration.
|
||||
*
|
||||
* @var array<string, array<int|string, mixed>>
|
||||
*/
|
||||
public private(set) array $postprocs = [];
|
||||
|
||||
/**
|
||||
* @param string $method HTTP method this route serves.
|
||||
|
@ -42,17 +51,31 @@ class RouteInfo {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds middleware to the pipeline prior execution of the handler. Executed in order of registration/
|
||||
* Adds a preprocessor to the pipeline after execution of the handler. Executed in order of registration.
|
||||
*
|
||||
* @param string $name Name of the middleware, implementation registered in the router.
|
||||
* @param array<int|string, mixed> $args Additional arguments to call the middleware with.
|
||||
* @param string $name Name of the preprocessor, implementation registered in the router.
|
||||
* @param array<int|string, mixed> $args Additional arguments to call the preprocessor with.
|
||||
* @throws InvalidArgumentException If $name has already been registered.
|
||||
*/
|
||||
public function require(string $name, array $args = []): void {
|
||||
if(array_key_exists($name, $this->middlewares))
|
||||
throw new InvalidArgumentException('middleware in $name has been registered for this route');
|
||||
public function before(string $name, array $args = []): void {
|
||||
if(array_key_exists($name, $this->preprocs))
|
||||
throw new InvalidArgumentException('pre-processor in $name has been registered for this route');
|
||||
|
||||
$this->middlewares[$name] = $args;
|
||||
$this->preprocs[$name] = $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a postprocessor to the pipeline after execution of the handler. Executed in order of registration.
|
||||
*
|
||||
* @param string $name Name of the postprocessor, implementation registered in the router.
|
||||
* @param array<int|string, mixed> $args Additional arguments to call the postprocessor with.
|
||||
* @throws InvalidArgumentException If $name has already been registered.
|
||||
*/
|
||||
public function after(string $name, array $args = []): void {
|
||||
if(array_key_exists($name, $this->postprocs))
|
||||
throw new InvalidArgumentException('post-processor in $name has been registered for this route');
|
||||
|
||||
$this->postprocs[$name] = $args;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,26 +5,26 @@
|
|||
|
||||
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,UriInterface};
|
||||
use Stringable;
|
||||
use JsonSerializable;
|
||||
use Index\Bencode\{Bencode,BencodeSerializable};
|
||||
use Index\Http\{HttpRequest,HttpStream};
|
||||
use Index\Http\Routing\ErrorHandling\{ErrorHandler,HtmlErrorHandler,PlainErrorHandler};
|
||||
use Psr\Http\Message\{ResponseInterface,ServerRequestInterface};
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* Implements a router for HTTP requests.
|
||||
*
|
||||
* There are three different kinds of handler you can register.
|
||||
* There are four different kinds of handler you can register.
|
||||
* - Filter: Always runs on a given path or prefix regardless of a route being matched.
|
||||
* - Middleware: Must be explicitly imported by a route handler. Middleware is used to transform the input or output of a route.
|
||||
* - Preprocessors: Must be explicitly imported by a route handler. Performs checks and can transform details of the pipeline.
|
||||
* - Route: An actual proper handler for a request.
|
||||
* - Postprocessors: Must be explicitly imported by a route handler. Performs transformations on the output.
|
||||
*
|
||||
* [HTTP Request] -> [Run Filters] -> [Run 'before' Middleware] -> [Run route handler] -> [Run 'after' Middleware] -> [HTTP Response]
|
||||
* [HTTP Request] -> [Run Filters] -> [Run Preprocessors] -> [Run route handler] -> [Run Postprocessors] -> [HTTP Response]
|
||||
*
|
||||
* 'before' middleware has the same abilities as filters do, but as mentioned are included explicitly.
|
||||
* Preprocessors has the same abilities as filters do, but as mentioned are included explicitly.
|
||||
*/
|
||||
class Router implements RequestHandlerInterface {
|
||||
/** @var FilterInfo[] */
|
||||
|
@ -37,11 +37,11 @@ class Router implements RequestHandlerInterface {
|
|||
|
||||
/**
|
||||
* @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.
|
||||
* @param ErrorHandler|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'
|
||||
ErrorHandler|string $errorHandler = 'html'
|
||||
) {
|
||||
$this->charSetValue = $charSet;
|
||||
$this->errorHandler = $errorHandler;
|
||||
|
@ -69,12 +69,12 @@ class Router implements RequestHandlerInterface {
|
|||
/**
|
||||
* Error handler instance.
|
||||
*
|
||||
* @var HttpErrorHandler
|
||||
* @var ErrorHandler
|
||||
*/
|
||||
public HttpErrorHandler $errorHandler {
|
||||
public ErrorHandler $errorHandler {
|
||||
get => $this->errorHandler;
|
||||
set(HttpErrorHandler|string $handler) {
|
||||
if($handler instanceof HttpErrorHandler)
|
||||
set(ErrorHandler|string $handler) {
|
||||
if($handler instanceof ErrorHandler)
|
||||
$this->errorHandler = $handler;
|
||||
elseif($handler === 'html')
|
||||
$this->setHtmlErrorHandler();
|
||||
|
@ -87,14 +87,14 @@ class Router implements RequestHandlerInterface {
|
|||
* Set the error handler to the basic HTML one.
|
||||
*/
|
||||
public function setHtmlErrorHandler(): void {
|
||||
$this->errorHandler = new HtmlHttpErrorHandler;
|
||||
$this->errorHandler = new HtmlErrorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error handler to the plain text one.
|
||||
*/
|
||||
public function setPlainErrorHandler(): void {
|
||||
$this->errorHandler = new PlainHttpErrorHandler;
|
||||
$this->errorHandler = new PlainErrorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,108 +126,137 @@ class Router implements RequestHandlerInterface {
|
|||
$handler->registerRoutes($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default steps that get run at the end of the pipeline, including default return value handling.
|
||||
*
|
||||
* @param HandlerContext $context Route pipeline context instance.
|
||||
* @param mixed $result The last return value.
|
||||
*/
|
||||
public function defaultResultHandler(HandlerContext $context, mixed $result): void {
|
||||
if($result !== null) {
|
||||
if(is_int($result)) {
|
||||
if($result < 100 || $result > 599) {
|
||||
$context->response->statusCode = 500;
|
||||
$context->response->reasonPhrase = 'Unsupported Status Code';
|
||||
} else {
|
||||
$context->response->statusCode = $result;
|
||||
$context->response->reasonPhrase = '';
|
||||
}
|
||||
|
||||
$this->errorHandler->handle($context);
|
||||
} elseif($context->response->needsBody) {
|
||||
// this is a slight step back, but should be remedied by using postprocessors
|
||||
if($result instanceof JsonSerializable) {
|
||||
if(!$context->response->hasContentType())
|
||||
$context->response->setTypeJson();
|
||||
|
||||
$result = json_encode($result, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
} elseif($result instanceof BencodeSerializable) {
|
||||
if(!$context->response->hasContentType())
|
||||
$context->response->setTypeBencode();
|
||||
|
||||
$result = Bencode::encode($result);
|
||||
} elseif($result instanceof Stringable) {
|
||||
$result = (string)$result;
|
||||
} elseif(is_object($result) || is_array($result)) {
|
||||
if(!$context->response->hasContentType())
|
||||
$context->response->setTypeJson();
|
||||
|
||||
$result = json_encode($result, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
} elseif(is_scalar($result)) {
|
||||
$result = (string)$result;
|
||||
} else {
|
||||
$result = '';
|
||||
}
|
||||
|
||||
if(!$context->response->hasContentType()) {
|
||||
if(mb_stripos($result, '<!doctype html') === 0)
|
||||
$context->response->setTypeHtml($this->charSet);
|
||||
elseif(mb_stripos($result, '<?xml') === 0)
|
||||
$context->response->setTypeXml($this->charSet);
|
||||
else
|
||||
$context->response->setTypePlain($this->charSet);
|
||||
}
|
||||
|
||||
$context->response->body = HttpStream::createStream($result);
|
||||
}
|
||||
}
|
||||
|
||||
if(!$context->response->hasContentLength()) {
|
||||
$bytes = $context->response->body?->getSize();
|
||||
if($bytes !== null)
|
||||
$context->response->setContentLength($bytes);
|
||||
}
|
||||
|
||||
if($context->request->method === 'HEAD')
|
||||
$context->response->body = null;
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface {
|
||||
$context = new HandlerContext($request);
|
||||
$args = [$context->response, $context->request];
|
||||
|
||||
$filters = [];
|
||||
foreach($this->filters as $filterInfo) {
|
||||
$match = $filterInfo->matcher->match($context->request->uri);
|
||||
if($match !== false) {
|
||||
$info = ['info' => $filterInfo];
|
||||
if(is_array($match))
|
||||
$info['matches'] = $match;
|
||||
|
||||
//$filters[] = $info;
|
||||
|
||||
$filters[] = [$filterInfo->handler, is_array($match) ? $match : []];
|
||||
$result = $context->call($filterInfo->handler, is_array($match) ? $match : null);
|
||||
if($result !== null || $context->stopped) {
|
||||
$this->defaultResultHandler($context, $result);
|
||||
return $context->response->toResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$methods = []; // legacy
|
||||
|
||||
$routes = [];
|
||||
$methods = [];
|
||||
foreach($this->routes as $routeInfo) {
|
||||
$match = $routeInfo->matcher->match($context->request->uri);
|
||||
if($match !== false) {
|
||||
$info = ['info' => $routeInfo];
|
||||
if(is_array($match))
|
||||
$info['matches'] = $match;
|
||||
$routes[] = $info;
|
||||
if($match !== false)
|
||||
$methods[$routeInfo->method] = [
|
||||
'info' => $routeInfo,
|
||||
'matches' => is_array($match) ? $match : null,
|
||||
];
|
||||
}
|
||||
|
||||
$methods[$routeInfo->method] = [$routeInfo->handler, is_array($match) ? $match : []];
|
||||
if(empty($methods)) {
|
||||
$context->response->statusCode = 404;
|
||||
$context->response->reasonPhrase = '';
|
||||
return $context->response->toResponse();
|
||||
}
|
||||
|
||||
$getAllowedMethods = function() use ($methods) {
|
||||
$allow = array_keys($methods);
|
||||
$allow[] = 'OPTIONS';
|
||||
if(in_array('GET', $allow))
|
||||
$allow[] = 'HEAD';
|
||||
|
||||
$allow = array_unique($allow);
|
||||
sort($allow);
|
||||
|
||||
return $allow;
|
||||
};
|
||||
|
||||
if(!array_key_exists($context->request->method, $methods)) {
|
||||
if($context->request->method === 'OPTIONS') {
|
||||
$context->response->statusCode = 204;
|
||||
$context->response->reasonPhrase = '';
|
||||
$context->response->setAllow($getAllowedMethods());
|
||||
return $context->response->toResponse();
|
||||
} elseif($context->request->method === 'HEAD' && array_key_exists('GET', $methods)) {
|
||||
$methods['HEAD'] = $methods['GET'];
|
||||
} else {
|
||||
$context->response->statusCode = 405;
|
||||
$context->response->reasonPhrase = '';
|
||||
$context->response->setAllow($getAllowedMethods());
|
||||
return $context->response->toResponse();
|
||||
}
|
||||
}
|
||||
|
||||
$method = strtoupper($context->request->method);
|
||||
if(array_key_exists($method, $methods)) {
|
||||
[$handler, $args] = $methods[$method];
|
||||
} elseif($method === 'OPTIONS') {
|
||||
// this should be implemented differently and also handle the CORS headers
|
||||
$supportedMethods = array_keys($methods);
|
||||
$supportedMethods[] = 'OPTIONS';
|
||||
if(in_array('GET', $supportedMethods))
|
||||
$supportedMethods[] = 'HEAD';
|
||||
$route = $methods[$context->request->method];
|
||||
$result = $context->call($route['info']->handler, $route['matches']);
|
||||
|
||||
$supportedMethods = array_unique($supportedMethods);
|
||||
sort($supportedMethods);
|
||||
// wait for the pipeline stuff
|
||||
//if($result !== null || $context->stopped)
|
||||
|
||||
$args[] = $supportedMethods;
|
||||
$handler = function(HttpResponseBuilder $response, HttpRequest $request, array $methods) {
|
||||
$response->statusCode = 204;
|
||||
$response->setHeader('Allow', implode(', ', $methods));
|
||||
};
|
||||
} elseif($method === 'HEAD' && array_key_exists('GET', $methods)) {
|
||||
[$handler, $args] = $methods['GET'];
|
||||
} else {
|
||||
$handler = null;
|
||||
$args = [];
|
||||
}
|
||||
|
||||
$routeInfo = new ResolvedRouteInfo($filters, array_keys($methods), $handler, $args);
|
||||
|
||||
// always run filters regardless of 404 or 405
|
||||
$result = $routeInfo->runFilters($args);
|
||||
if($result === null) {
|
||||
if(!$routeInfo->hasHandler()) {
|
||||
if(empty($routeInfo->supportedMethods)) {
|
||||
$result = 404;
|
||||
} else {
|
||||
$result = 405;
|
||||
$context->response->setHeader('Allow', implode(', ', $routeInfo->supportedMethods));
|
||||
}
|
||||
} else
|
||||
$result = $routeInfo->dispatch($args);
|
||||
}
|
||||
|
||||
if(is_int($result)) {
|
||||
if($result >= 100 && $result < 600)
|
||||
$this->writeErrorPage($context->response, $context->request, $result);
|
||||
} elseif($context->response->body === null) {
|
||||
if(is_scalar($result)) {
|
||||
$result = (string)$result;
|
||||
$context->response->body = HttpStream::createStream($result);
|
||||
|
||||
if(!$context->response->hasContentType()) {
|
||||
if(strtolower(substr($result, 0, 14)) === '<!doctype html')
|
||||
$context->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')
|
||||
$context->response->setTypeXml($charset);
|
||||
else
|
||||
$context->response->setTypePlain($charset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($context->request->method === 'HEAD' && $context->response->body !== null)
|
||||
$context->response->body = null;
|
||||
$this->defaultResultHandler($context, $result);
|
||||
|
||||
return $context->response->toResponse();
|
||||
}
|
||||
|
@ -241,22 +270,6 @@ class Router implements RequestHandlerInterface {
|
|||
self::output($this->handle($request ?? HttpRequest::fromRequest()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue