Redid base route pipeline.

This commit is contained in:
flash 2025-03-07 20:01:27 +00:00
parent d0b9b2d556
commit bc43662792
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
12 changed files with 372 additions and 238 deletions

View file

@ -1 +1 @@
0.2503.70242
0.2503.72000

View file

@ -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,
]));
}
}

View file

@ -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;
}

View file

@ -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 = [];
}

View file

@ -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.
*

View file

@ -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.
*/

View file

@ -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));
}
}

View 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;
}

View 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,
]));
}
}

View 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
));
}
}

View file

@ -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;
}
/**

View file

@ -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.
*