Added content and error handling.

This commit is contained in:
flash 2024-03-28 22:30:06 +00:00
parent add621716a
commit ce855061ec
10 changed files with 275 additions and 3 deletions

View file

@ -1 +1 @@
0.2403.282033
0.2403.282229

View file

@ -0,0 +1,23 @@
<?php
// BencodeContentHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\ContentHandling;
use Index\Http\HttpResponseBuilder;
use Index\Http\Content\BencodedContent;
use Index\Serialisation\IBencodeSerialisable;
class BencodeContentHandler implements IContentHandler {
public function match(mixed $content): bool {
return $content instanceof IBencodeSerialisable;
}
public function handle(HttpResponseBuilder $response, mixed $content): void {
if(!$response->hasContentType())
$response->setTypePlain();
$response->setContent(new BencodedContent($content));
}
}

View file

@ -0,0 +1,12 @@
<?php
// IContentHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\ContentHandling;
use Index\Http\HttpResponseBuilder;
interface IContentHandler {
function match(mixed $content): bool;
function handle(HttpResponseBuilder $response, mixed $content): void;
}

View file

@ -0,0 +1,24 @@
<?php
// JsonContentHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\ContentHandling;
use stdClass;
use JsonSerializable;
use Index\Http\HttpResponseBuilder;
use Index\Http\Content\JsonContent;
class JsonContentHandler implements IContentHandler {
public function match(mixed $content): bool {
return is_array($content) || $content instanceof JsonSerializable || $content instanceof stdClass;
}
public function handle(HttpResponseBuilder $response, mixed $content): void {
if(!$response->hasContentType())
$response->setTypeJson();
$response->setContent(new JsonContent($content));
}
}

View file

@ -0,0 +1,23 @@
<?php
// StreamContentHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\ContentHandling;
use Index\Http\HttpResponseBuilder;
use Index\Http\Content\StreamContent;
use Index\IO\Stream;
class StreamContentHandler implements IContentHandler {
public function match(mixed $content): bool {
return $content instanceof Stream;
}
public function handle(HttpResponseBuilder $response, mixed $content): void {
if(!$response->hasContentType())
$response->setTypeStream();
$response->setContent(new StreamContent($content));
}
}

View file

@ -0,0 +1,33 @@
<?php
// HtmlErrorHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\ErrorHandling;
use Index\Http\{HttpResponseBuilder,HttpRequest};
class HtmlErrorHandler implements IErrorHandler {
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, HttpRequest $request, int $code, string $message): void {
$response->setTypeHTML();
$response->setContent(strtr(self::TEMPLATE, [
':charset' => strtolower(mb_preferred_mime_name(mb_internal_encoding())),
':code' => sprintf('%03d', $code),
':message' => $message,
]));
}
}

View file

@ -0,0 +1,13 @@
<?php
// IErrorHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\ErrorHandling;
use Index\Http\HttpResponseBuilder;
use Index\Http\HttpRequest;
interface IErrorHandler {
function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void;
}

View file

@ -0,0 +1,14 @@
<?php
// PlainErrorHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\ErrorHandling;
use Index\Http\{HttpResponseBuilder,HttpRequest};
class PlainErrorHandler implements IErrorHandler {
public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
$response->setTypePlain();
$response->setContent(sprintf('HTTP %03d %s', $code, $message));
}
}

View file

@ -1,7 +1,7 @@
<?php
// HttpMessageBuilder.php
// Created: 2022-02-08
// Updated: 2022-02-27
// Updated: 2024-03-28
namespace Index\Http;
@ -56,6 +56,7 @@ class HttpMessageBuilder {
return $this->content;
}
/** @phpstan-impure */
public function hasContent(): bool {
return $this->content !== null;
}

View file

@ -7,7 +7,10 @@ namespace Index\Http\Routing;
use stdClass;
use InvalidArgumentException;
use RuntimeException;
use Index\Http\{HttpResponse,HttpResponseBuilder,HttpRequest};
use Index\Http\Content\StringContent;
use Index\Http\ContentHandling\{BencodeContentHandler,IContentHandler,JsonContentHandler,StreamContentHandler};
use Index\Http\ErrorHandling\{HtmlErrorHandler,IErrorHandler,PlainErrorHandler};
class HttpRouter implements IRouter {
use RouterTrait;
@ -17,6 +20,52 @@ class HttpRouter implements IRouter {
private array $staticRoutes = [];
private array $dynamicRoutes = [];
private array $contentHandlers = [];
private IErrorHandler $errorHandler;
public function __construct(
IErrorHandler|string $errorHandler = 'html',
bool $registerDefaultContentHandlers = true,
) {
$this->setErrorHandler($errorHandler);
if($registerDefaultContentHandlers)
$this->registerDefaultContentHandlers();
}
public function getErrorHandler(): IErrorHandler {
return $this->errorHandler;
}
public function setErrorHandler(IErrorHandler|string $handler): void {
if($handler instanceof IErrorHandler)
$this->errorHandler = $handler;
elseif($handler === 'html')
$this->setHTMLErrorHandler();
else // plain
$this->setPlainErrorHandler();
}
public function setHTMLErrorHandler(): void {
$this->errorHandler = new HtmlErrorHandler;
}
public function setPlainErrorHandler(): void {
$this->errorHandler = new PlainErrorHandler;
}
public function registerContentHandler(IContentHandler $contentHandler): void {
if(!in_array($contentHandler, $this->contentHandlers))
$this->contentHandlers[] = $contentHandler;
}
public function registerDefaultContentHandlers(): void {
$this->registerContentHandler(new StreamContentHandler);
$this->registerContentHandler(new JsonContentHandler);
$this->registerContentHandler(new BencodeContentHandler);
}
public function scopeTo(string $prefix): IRouter {
return new ScopedRouter($this, $prefix);
}
@ -104,4 +153,84 @@ class HttpRouter implements IRouter {
return new ResolvedRouteInfo($middlewares, array_keys($methods), $handler, $args);
}
public function dispatch(?HttpRequest $request = null, array $args = []): void {
$request ??= HttpRequest::fromRequest();
$response = new HttpResponseBuilder;
$args = array_merge([$response, $request], $args);
$routeInfo = $this->resolve($request->getMethod(), $request->getPath());
// always run middleware regardless of 404 or 405
$result = $routeInfo->runMiddleware($args);
if($result === null) {
if(!$routeInfo->hasHandler()) {
if($routeInfo->hasOtherMethods()) {
$result = 405;
$response->setHeader('Allow', implode(', ', $routeInfo->getSupportedMethods()));
} else
$result = 404;
} else
$result = $routeInfo->dispatch($args);
}
if(is_int($result)) {
if(!$response->hasStatusCode() && $result >= 100 && $result < 600)
$this->writeErrorPage($response, $request, $result);
else
$response->setContent(new StringContent((string)$result));
} elseif(!$response->hasContent()) {
foreach($this->contentHandlers as $contentHandler)
if($contentHandler->match($result)) {
$contentHandler->handle($response, $result);
break;
}
if(!$response->hasContent() && $result !== null) {
$result = (string)$result;
$response->setContent(new StringContent($result));
if(!$response->hasContentType()) {
$charset = strtolower(mb_preferred_mime_name(mb_detect_encoding($result)));
if(strtolower(substr($result, 0, 14)) === '<!doctype html')
$response->setTypeHTML($charset);
elseif(strtolower(substr($result, 0, 5)) === '<?xml')
$response->setTypeXML($charset);
else
$response->setTypePlain($charset);
}
}
}
self::output($response->toResponse());
}
public function writeErrorPage(HttpResponseBuilder $response, HttpRequest $request, int $statusCode): void {
$response->setStatusCode($statusCode);
$this->errorHandler->handle($response, $request, $response->getStatusCode(), $response->getStatusText());
}
public static function output(HttpResponse $response): void {
$version = $response->getHttpVersion();
header(sprintf(
'HTTP/%d.%d %03d %s',
$version->getMajor(),
$version->getMinor(),
$response->getStatusCode(),
$response->getStatusText()
));
$headers = $response->getHeaders();
foreach($headers as $header) {
$name = (string)$header->getName();
$lines = $header->getLines();
foreach($lines as $line)
header(sprintf('%s: %s', $name, (string)$line));
}
if($response->hasContent())
echo (string)$response->getContent();
}
}