2022-09-13 13:13:11 +00:00
|
|
|
<?php
|
|
|
|
// HttpFx.php
|
|
|
|
// Created: 2022-02-15
|
2023-01-06 20:20:59 +00:00
|
|
|
// Updated: 2023-01-06
|
2022-09-13 13:13:11 +00:00
|
|
|
|
|
|
|
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;
|
2023-01-06 20:20:59 +00:00
|
|
|
use Index\Routing\IRouter;
|
2022-09-13 13:13:11 +00:00
|
|
|
use Index\Routing\Router;
|
|
|
|
use Index\Routing\RoutePathNotFoundException;
|
|
|
|
use Index\Routing\RouteMethodNotSupportedException;
|
|
|
|
use Index\Serialisation\IBencodeSerialisable;
|
|
|
|
|
2023-01-06 20:20:59 +00:00
|
|
|
class HttpFx implements IRouter {
|
2022-09-13 13:13:11 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|