Added content and error handling.
This commit is contained in:
parent
add621716a
commit
ce855061ec
10 changed files with 275 additions and 3 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2403.282033
|
||||
0.2403.282229
|
||||
|
|
23
src/Http/ContentHandling/BencodeContentHandler.php
Normal file
23
src/Http/ContentHandling/BencodeContentHandler.php
Normal 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));
|
||||
}
|
||||
}
|
12
src/Http/ContentHandling/IContentHandler.php
Normal file
12
src/Http/ContentHandling/IContentHandler.php
Normal 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;
|
||||
}
|
24
src/Http/ContentHandling/JsonContentHandler.php
Normal file
24
src/Http/ContentHandling/JsonContentHandler.php
Normal 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));
|
||||
}
|
||||
}
|
23
src/Http/ContentHandling/StreamContentHandler.php
Normal file
23
src/Http/ContentHandling/StreamContentHandler.php
Normal 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));
|
||||
}
|
||||
}
|
33
src/Http/ErrorHandling/HtmlErrorHandler.php
Normal file
33
src/Http/ErrorHandling/HtmlErrorHandler.php
Normal 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,
|
||||
]));
|
||||
}
|
||||
}
|
13
src/Http/ErrorHandling/IErrorHandler.php
Normal file
13
src/Http/ErrorHandling/IErrorHandler.php
Normal 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;
|
||||
}
|
14
src/Http/ErrorHandling/PlainErrorHandler.php
Normal file
14
src/Http/ErrorHandling/PlainErrorHandler.php
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue