index/src/Http/HttpFx.php

290 lines
9.7 KiB
PHP

<?php
// HttpFx.php
// Created: 2022-02-15
// Updated: 2023-01-06
namespace Index\Http;
use stdClass;
use Exception;
use JsonSerializable;
use Index\Environment;
use Index\IString;
use Index\WString;
use Index\IO\Stream;
use Index\Http\Content\BencodedContent;
use Index\Http\Content\JsonContent;
use Index\Http\Content\StreamContent;
use Index\Http\Content\StringContent;
use Index\Routing\IRouter;
use Index\Routing\Router;
use Index\Routing\RoutePathNotFoundException;
use Index\Routing\RouteMethodNotSupportedException;
use Index\Serialisation\IBencodeSerialisable;
class HttpFx implements IRouter {
private Router $router;
private array $objectHandlers = [];
private array $errorHandlers = [];
private $defaultErrorHandler; // callable
public function __construct(?Router $router = null) {
self::restoreDefaultErrorHandler();
$this->router = $router ?? new Router;
$this->addObjectHandler(
fn(object $object) => $object instanceof Stream,
function(HttpResponseBuilder $responseBuilder, object $object) {
if(!$responseBuilder->hasContentType())
$responseBuilder->setTypeStream();
$responseBuilder->setContent(new StreamContent($object));
}
);
$this->addObjectHandler(
fn(object $object) => $object instanceof JsonSerializable || $object instanceof stdClass,
function(HttpResponseBuilder $responseBuilder, object $object) {
if(!$responseBuilder->hasContentType())
$responseBuilder->setTypeJson();
$responseBuilder->setContent(new JsonContent($object));
}
);
$this->addObjectHandler(
fn(object $object) => $object instanceof IBencodeSerialisable,
function(HttpResponseBuilder $responseBuilder, object $object) {
if(!$responseBuilder->hasContentType())
$responseBuilder->setTypePlain();
$responseBuilder->setContent(new BencodedContent($object));
}
);
}
public function getRouter(): Router {
return $this->router;
}
public function addObjectHandler(callable $match, callable $handler): void {
$this->objectHandlers[] = [$match, $handler];
}
public function addErrorHandler(int $code, callable $handler): void {
$this->errorHandlers[$code] = $handler;
}
public function setDefaultErrorHandler(callable $handler): void {
$this->defaultErrorHandler = $handler;
}
public function restoreDefaultErrorHandler(): void {
$this->defaultErrorHandler = [self::class, 'defaultErrorHandler'];
}
public function dispatch(?HttpRequest $request = null, array $args = []): void {
$request ??= HttpRequest::fromRequest();
$responseBuilder = new HttpResponseBuilder;
$handlers = null;
try {
$handlers = $this->router->resolve($request->getMethod(), $request->getPath(), array_merge([
$responseBuilder, $request,
], $args));
} catch(RoutePathNotFoundException $ex) {
$statusCode = 404;
} catch(RouteMethodNotSupportedException $ex) {
$statusCode = 405;
} catch(Exception $ex) {
if(Environment::isDebug())
throw $ex;
}
if($handlers === null) {
$this->errorPage($responseBuilder, $request, $statusCode ?? 500);
} else {
$result = $handlers->run();
if(is_int($result)) {
if(!$responseBuilder->hasStatusCode() && $result >= 100 && $result < 600) {
$this->errorPage($responseBuilder, $request, $result);
} elseif(!$responseBuilder->hasContent()) {
$responseBuilder->setContent(new StringContent((string)$result));
}
} elseif(!$responseBuilder->hasContent()) {
if(is_array($result)) {
if(!$responseBuilder->hasContentType())
$responseBuilder->setTypeJson();
$responseBuilder->setContent(new JsonContent($result));
} elseif(is_object($result) && !($result instanceof IString)) {
foreach($this->objectHandlers as $info)
if($info[0]($result)) {
$info[1]($responseBuilder, $result);
break;
}
}
if(!$responseBuilder->hasContent() && $result !== null) {
$charset = ($result instanceof WString) ? $result->getEncoding() : null;
$result = (string)$result;
$responseBuilder->setContent(new StringContent($result));
if(!$responseBuilder->hasContentType()) {
if(strtolower(substr($result, 0, 5)) === '<?xml')
$responseBuilder->setTypeXML($charset ?? WString::getDefaultEncoding());
elseif(strtolower(substr($result, 0, 14)) === '<!doctype html')
$responseBuilder->setTypeHTML($charset ?? WString::getDefaultEncoding());
else
$responseBuilder->setTypePlain($charset ?? 'us-ascii');
}
}
}
}
self::output($responseBuilder->toResponse());
}
public static function defaultErrorHandler(
HttpResponseBuilder $responseBuilder,
HttpRequest $request,
int $code,
string $message
): void {
$responseBuilder->setTypeHTML();
$responseBuilder->setContent(new StringContent(sprintf(
'<!doctype html><html><head><meta charset="%3$s"/><title>%1$03d %2$s</title></head><body><center><h1>%1$03d %2$s</h1></center><hr/><center>Index</center></body></html>',
$code,
$message,
WString::getDefaultEncoding()
)));
}
public function errorPage(
HttpResponseBuilder $responseBuilder,
HttpRequest $request,
int $statusCode
): void {
$responseBuilder->setStatusCode($statusCode);
$responseBuilder->clearStatusText();
if(!$responseBuilder->hasContent())
($this->errorHandlers[$statusCode] ?? $this->defaultErrorHandler)(
$responseBuilder,
$request,
$responseBuilder->getStatusCode(),
$responseBuilder->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();
}
/**
* Apply middleware functions to a path.
*
* @param string $path Path to apply the middleware to.
* @param callable $handler Middleware function.
*/
public function use(string $path, callable $handler): void {
$this->router->use($path, $handler);
}
/**
* Merges another router with this one with possibility of changing its root.
*
* @param string $path Base path to use.
* @param Router $router Router object to inherit from.
*/
public function merge(string $path, Router $router): void {
$this->router->merge($path, $router);
}
/**
* Adds a new route.
*
* @param string $method Request method.
* @param string $path Request path.
* @param callable $handler Request handler.
*/
public function add(string $method, string $path, callable $handler): void {
$this->router->add($method, $path, $handler);
}
/**
* Adds a new GET route.
*
* @param string $path Request path.
* @param callable $handler Request handler.
*/
public function get(string $path, callable $handler): void {
$this->router->add('get', $path, $handler);
}
/**
* Adds a new POST route.
*
* @param string $path Request path.
* @param callable $handler Request handler.
*/
public function post(string $path, callable $handler): void {
$this->router->add('post', $path, $handler);
}
/**
* Adds a new DELETE route.
*
* @param string $path Request path.
* @param callable $handler Request handler.
*/
public function delete(string $path, callable $handler): void {
$this->router->add('delete', $path, $handler);
}
/**
* Adds a new PATCH route.
*
* @param string $path Request path.
* @param callable $handler Request handler.
*/
public function patch(string $path, callable $handler): void {
$this->router->add('patch', $path, $handler);
}
/**
* Adds a new PUT route.
*
* @param string $path Request path.
* @param callable $handler Request handler.
*/
public function put(string $path, callable $handler): void {
$this->router->add('put', $path, $handler);
}
/**
* Adds a new OPTIONS route.
*
* @param string $path Request path.
* @param callable $handler Request handler.
*/
public function options(string $path, callable $handler): void {
$this->router->add('options', $path, $handler);
}
}