Removed old router code.
This commit is contained in:
parent
9d5b050b89
commit
9b57fbb42c
|
@ -1,290 +0,0 @@
|
||||||
<?php
|
|
||||||
// HttpFx.php
|
|
||||||
// Created: 2022-02-15
|
|
||||||
// Updated: 2023-11-20
|
|
||||||
|
|
||||||
namespace Index\Http;
|
|
||||||
|
|
||||||
use stdClass;
|
|
||||||
use Exception;
|
|
||||||
use JsonSerializable;
|
|
||||||
use Index\Environment;
|
|
||||||
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\IRouteHandler;
|
|
||||||
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)) {
|
|
||||||
foreach($this->objectHandlers as $info)
|
|
||||||
if($info[0]($result)) {
|
|
||||||
$info[1]($responseBuilder, $result);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!$responseBuilder->hasContent() && $result !== null) {
|
|
||||||
$result = (string)$result;
|
|
||||||
$responseBuilder->setContent(new StringContent($result));
|
|
||||||
|
|
||||||
if(!$responseBuilder->hasContentType()) {
|
|
||||||
if(strtolower(substr($result, 0, 14)) === '<!doctype html')
|
|
||||||
$responseBuilder->setTypeHTML('utf-8');
|
|
||||||
else {
|
|
||||||
$charset = strtolower(mb_preferred_mime_name(mb_detect_encoding($result)));
|
|
||||||
|
|
||||||
if(strtolower(substr($result, 0, 5)) === '<?xml')
|
|
||||||
$responseBuilder->setTypeXML($charset);
|
|
||||||
else
|
|
||||||
$responseBuilder->setTypePlain($charset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
strtolower(mb_preferred_mime_name(mb_internal_encoding()))
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers routes in an IRouteHandler implementation.
|
|
||||||
*
|
|
||||||
* @param IRouteHandler $handler Routes handler.
|
|
||||||
*/
|
|
||||||
public function register(IRouteHandler $handler): void {
|
|
||||||
$handler->registerRoutes($this);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
// IRouteHandler.php
|
|
||||||
// Created: 2023-09-06
|
|
||||||
// Updated: 2023-09-07
|
|
||||||
|
|
||||||
namespace Index\Routing;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the interface for IRouter::register().
|
|
||||||
*/
|
|
||||||
interface IRouteHandler {
|
|
||||||
/**
|
|
||||||
* Registers routes on a given IRouter instance.
|
|
||||||
*
|
|
||||||
* @param IRouter $router Target router.
|
|
||||||
*/
|
|
||||||
public function registerRoutes(IRouter $router): void;
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
<?php
|
|
||||||
// IRouter.php
|
|
||||||
// Created: 2023-01-06
|
|
||||||
// Updated: 2023-09-11
|
|
||||||
|
|
||||||
namespace Index\Routing;
|
|
||||||
|
|
||||||
interface IRouter {
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new GET route.
|
|
||||||
*
|
|
||||||
* @param string $path Request path.
|
|
||||||
* @param callable $handler Request handler.
|
|
||||||
*/
|
|
||||||
public function get(string $path, callable $handler): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new POST route.
|
|
||||||
*
|
|
||||||
* @param string $path Request path.
|
|
||||||
* @param callable $handler Request handler.
|
|
||||||
*/
|
|
||||||
public function post(string $path, callable $handler): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new DELETE route.
|
|
||||||
*
|
|
||||||
* @param string $path Request path.
|
|
||||||
* @param callable $handler Request handler.
|
|
||||||
*/
|
|
||||||
public function delete(string $path, callable $handler): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new PATCH route.
|
|
||||||
*
|
|
||||||
* @param string $path Request path.
|
|
||||||
* @param callable $handler Request handler.
|
|
||||||
*/
|
|
||||||
public function patch(string $path, callable $handler): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new PUT route.
|
|
||||||
*
|
|
||||||
* @param string $path Request path.
|
|
||||||
* @param callable $handler Request handler.
|
|
||||||
*/
|
|
||||||
public function put(string $path, callable $handler): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new OPTIONS route.
|
|
||||||
*
|
|
||||||
* @param string $path Request path.
|
|
||||||
* @param callable $handler Request handler.
|
|
||||||
*/
|
|
||||||
public function options(string $path, callable $handler): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers routes in an IRouteHandler implementation.
|
|
||||||
*
|
|
||||||
* @param IRouteHandler $handler Routes handler.
|
|
||||||
*/
|
|
||||||
public function register(IRouteHandler $handler): void;
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
<?php
|
|
||||||
// Route.php
|
|
||||||
// Created: 2023-09-07
|
|
||||||
// Updated: 2023-09-08
|
|
||||||
|
|
||||||
namespace Index\Routing;
|
|
||||||
|
|
||||||
use Attribute;
|
|
||||||
use ReflectionObject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides an attribute for marking methods in a class as routes.
|
|
||||||
*/
|
|
||||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
|
||||||
class Route {
|
|
||||||
private ?string $method;
|
|
||||||
private string $path;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Route attribute.
|
|
||||||
*
|
|
||||||
* @param string $pathOrMethod Method name if this is registering a route, path if this is registering middleware.
|
|
||||||
* @param ?string $path Path if this registering a route, null if this is registering middleware.
|
|
||||||
*/
|
|
||||||
public function __construct(string $pathOrMethod, ?string $path = null) {
|
|
||||||
if($path === null) {
|
|
||||||
$this->method = null;
|
|
||||||
$this->path = $pathOrMethod;
|
|
||||||
} else {
|
|
||||||
$this->method = $pathOrMethod;
|
|
||||||
$this->path = $path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the target method name.
|
|
||||||
*
|
|
||||||
* @return ?string
|
|
||||||
*/
|
|
||||||
public function getMethod(): ?string {
|
|
||||||
return $this->method;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this route should be used as middleware.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isMiddleware(): bool {
|
|
||||||
return $this->method === null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the target path.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getPath(): string {
|
|
||||||
return $this->path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads attributes from methods in a IRouteHandler instance and registers them to a given IRouter instance.
|
|
||||||
*
|
|
||||||
* @param IRouter $router Router instance.
|
|
||||||
* @param IRouteHandler $handler Handler instance.
|
|
||||||
*/
|
|
||||||
public static function handleAttributes(IRouter $router, IRouteHandler $handler): void {
|
|
||||||
$objectInfo = new ReflectionObject($handler);
|
|
||||||
$methodInfos = $objectInfo->getMethods();
|
|
||||||
|
|
||||||
foreach($methodInfos as $methodInfo) {
|
|
||||||
$attrInfos = $methodInfo->getAttributes(Route::class);
|
|
||||||
|
|
||||||
foreach($attrInfos as $attrInfo) {
|
|
||||||
$routeInfo = $attrInfo->newInstance();
|
|
||||||
$closure = $methodInfo->getClosure($methodInfo->isStatic() ? null : $handler);
|
|
||||||
|
|
||||||
if($routeInfo->isMiddleware())
|
|
||||||
$router->use($routeInfo->getPath(), $closure);
|
|
||||||
else
|
|
||||||
$router->add($routeInfo->getMethod(), $routeInfo->getPath(), $closure);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
<?php
|
|
||||||
// RouteCallable.php
|
|
||||||
// Created: 2022-01-20
|
|
||||||
// Updated: 2022-02-02
|
|
||||||
|
|
||||||
namespace Index\Routing;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stack of callables and arguments for route responses.
|
|
||||||
*/
|
|
||||||
class RouteCallable {
|
|
||||||
private array $callables;
|
|
||||||
private array $args;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
public function __construct(array $callables, array $args) {
|
|
||||||
$this->callables = $callables;
|
|
||||||
$this->args = $args;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callables in order that they should be executed.
|
|
||||||
*
|
|
||||||
* @return array Sequential list of callables.
|
|
||||||
*/
|
|
||||||
public function getCallables(): array {
|
|
||||||
return $this->callables;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Arguments to be sent to each callable.
|
|
||||||
*
|
|
||||||
* @return array Sequential argument list for the callables.
|
|
||||||
*/
|
|
||||||
public function getArguments(): array {
|
|
||||||
return $this->args;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs all callables and returns their returns as an array.
|
|
||||||
*
|
|
||||||
* @return array Results from the callables.
|
|
||||||
*/
|
|
||||||
public function runAll(): array {
|
|
||||||
$results = [];
|
|
||||||
|
|
||||||
foreach($this->callables as $callable) {
|
|
||||||
$result = $callable(...$this->args);
|
|
||||||
if($result !== null)
|
|
||||||
$results[] = $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs all callables unless one returns something.
|
|
||||||
*
|
|
||||||
* @return mixed Result from the returning callable.
|
|
||||||
*/
|
|
||||||
public function run(): mixed {
|
|
||||||
foreach($this->callables as $callable) {
|
|
||||||
$result = $callable(...$this->args);
|
|
||||||
if($result !== null)
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
<?php
|
|
||||||
// RouteHandler.php
|
|
||||||
// Created: 2023-09-07
|
|
||||||
// Updated: 2023-09-07
|
|
||||||
|
|
||||||
namespace Index\Routing;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides an abstract class version of IRouteHandler that already includes the trait as well,
|
|
||||||
* letting you only have to use one use statement rather than two!
|
|
||||||
*/
|
|
||||||
abstract class RouteHandler implements IRouteHandler {
|
|
||||||
use RouteHandlerTrait;
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
// RouteHandlerTrait.php
|
|
||||||
// Created: 2023-09-07
|
|
||||||
// Updated: 2023-09-07
|
|
||||||
|
|
||||||
namespace Index\Routing;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides an implementation of IRouteHandler::registerRoutes that uses the attributes.
|
|
||||||
* For more advanced use, everything can be use'd separately and Route::handleAttributes called manually.
|
|
||||||
*/
|
|
||||||
trait RouteHandlerTrait {
|
|
||||||
public function registerRoutes(IRouter $router): void {
|
|
||||||
Route::handleAttributes($router, $this);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
<?php
|
|
||||||
// RouteInfo.php
|
|
||||||
// Created: 2022-01-20
|
|
||||||
// Updated: 2023-09-11
|
|
||||||
|
|
||||||
namespace Index\Routing;
|
|
||||||
|
|
||||||
use InvalidArgumentException;
|
|
||||||
use UnexpectedValueException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a node in the router structure.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
class RouteInfo {
|
|
||||||
private array $children = [];
|
|
||||||
private array $methods = [];
|
|
||||||
private array $middlewares = [];
|
|
||||||
private ?RouteInfo $dynamicChild = null;
|
|
||||||
|
|
||||||
private function getChild(string $path, ?string &$next = null): RouteInfo {
|
|
||||||
$parts = explode('/', $path, 2);
|
|
||||||
$name = '_' . $parts[0];
|
|
||||||
|
|
||||||
if(str_starts_with($parts[0], ':'))
|
|
||||||
$child = $this->dynamicChild ?? ($this->dynamicChild = new RouteInfo);
|
|
||||||
else
|
|
||||||
$child = $this->children[$name] ?? ($this->children[$name] = new RouteInfo);
|
|
||||||
|
|
||||||
$next = $parts[1] ?? '';
|
|
||||||
|
|
||||||
return $child;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
public function addMiddleware(string $path, callable $handler): void {
|
|
||||||
if($path === '') {
|
|
||||||
$this->middlewares[] = $handler;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->getChild($path, $next)->addMiddleware($next, $handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
public function addMethod(string $method, string $path, callable $handler): void {
|
|
||||||
if($path === '') {
|
|
||||||
$this->methods[strtolower($method)] = $handler;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->getChild($path, $next)->addMethod($method, $next, $handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
public function resolve(string $method, string $path, array $args = [], array $callables = []): RouteCallable {
|
|
||||||
if($path === '') {
|
|
||||||
$method = strtolower($method);
|
|
||||||
$handlers = [];
|
|
||||||
|
|
||||||
if(isset($this->methods[$method])) {
|
|
||||||
$handlers[] = $this->methods[$method];
|
|
||||||
} else {
|
|
||||||
if($method !== 'options') {
|
|
||||||
if(empty($this->methods))
|
|
||||||
throw new RoutePathNotFoundException;
|
|
||||||
throw new RouteMethodNotSupportedException;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RouteCallable(
|
|
||||||
array_merge($callables, $this->middlewares, $handlers),
|
|
||||||
$args
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts = explode('/', $path, 2);
|
|
||||||
$name = $parts[0];
|
|
||||||
$mName = '_' . $name;
|
|
||||||
|
|
||||||
foreach($this->children as $cName => $cObj)
|
|
||||||
if($cName === $mName) {
|
|
||||||
$child = $cObj;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!isset($child)) {
|
|
||||||
$args[] = $name;
|
|
||||||
$child = $this->dynamicChild;
|
|
||||||
}
|
|
||||||
|
|
||||||
if($child === null)
|
|
||||||
throw new RoutePathNotFoundException;
|
|
||||||
|
|
||||||
return $child->resolve(
|
|
||||||
$method,
|
|
||||||
$parts[1] ?? '',
|
|
||||||
$args,
|
|
||||||
array_merge($callables, $this->middlewares)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
<?php
|
|
||||||
// RouteMethodNotSupportedException.php
|
|
||||||
// Created: 2022-01-20
|
|
||||||
// Updated: 2022-01-20
|
|
||||||
|
|
||||||
namespace Index\Routing;
|
|
||||||
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception thrown when an unsupported request method is invoked. (HTTP 405)
|
|
||||||
*/
|
|
||||||
class RouteMethodNotSupportedException extends RuntimeException {}
|
|
|
@ -1,13 +0,0 @@
|
||||||
<?php
|
|
||||||
// RoutePathNotFoundException.php
|
|
||||||
// Created: 2022-01-20
|
|
||||||
// Updated: 2022-01-20
|
|
||||||
|
|
||||||
namespace Index\Routing;
|
|
||||||
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception thrown when a path does not resolve to a route. (HTTP 404)
|
|
||||||
*/
|
|
||||||
class RoutePathNotFoundException extends RuntimeException {}
|
|
|
@ -1,131 +0,0 @@
|
||||||
<?php
|
|
||||||
// Router.php
|
|
||||||
// Created: 2022-01-18
|
|
||||||
// Updated: 2023-09-11
|
|
||||||
|
|
||||||
namespace Index\Routing;
|
|
||||||
|
|
||||||
use InvalidArgumentException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides an application router.
|
|
||||||
*/
|
|
||||||
class Router implements IRouter {
|
|
||||||
private RouteInfo $route;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a Router object.
|
|
||||||
*/
|
|
||||||
public function __construct() {
|
|
||||||
$this->route = new RouteInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a request method and uri.
|
|
||||||
*
|
|
||||||
* @param string $method Request method.
|
|
||||||
* @param string $path Request path.
|
|
||||||
* @param array $args Arguments to be passed on to the callables.
|
|
||||||
* @return RouteCallable A collection of callables representing the route.
|
|
||||||
*/
|
|
||||||
public function resolve(string $method, string $path, array $args = []): RouteCallable {
|
|
||||||
$method = strtolower($method);
|
|
||||||
if($method === 'head')
|
|
||||||
$method = 'get';
|
|
||||||
|
|
||||||
return $this->route->resolve($method, $path, $args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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->route->addMiddleware($path, $handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
if(empty($method))
|
|
||||||
throw new InvalidArgumentException('$method may not be empty.');
|
|
||||||
|
|
||||||
$this->route->addMethod($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->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->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->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->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->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->add('options', $path, $handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers routes in an IRouteHandler implementation.
|
|
||||||
*
|
|
||||||
* @param IRouteHandler $handler Routes handler.
|
|
||||||
*/
|
|
||||||
public function register(IRouteHandler $handler): void {
|
|
||||||
$handler->registerRoutes($this);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,144 +1,115 @@
|
||||||
<?php
|
<?php
|
||||||
// RouterTest.php
|
// RouterTest.php
|
||||||
// Created: 2022-01-20
|
// Created: 2022-01-20
|
||||||
// Updated: 2023-09-11
|
// Updated: 2024-03-30
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Index\Routing\Route;
|
use Index\Routing\Route;
|
||||||
use Index\Routing\RouteHandler;
|
|
||||||
use Index\Routing\Router;
|
use Index\Routing\Router;
|
||||||
|
use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,HttpPut,HttpRouter,RouteHandler};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers Router
|
* This test isn't super representative of the current functionality
|
||||||
|
* it mostly just does the same tests that were done against the previous implementation
|
||||||
|
*
|
||||||
|
* @covers HttpRouter
|
||||||
*/
|
*/
|
||||||
final class RouterTest extends TestCase {
|
final class RouterTest extends TestCase {
|
||||||
public function testRouter(): void {
|
public function testRouter(): void {
|
||||||
$router1 = new Router;
|
$router1 = new HttpRouter;
|
||||||
|
|
||||||
$router1->get('/', function() {
|
$router1->get('/', fn() => 'get');
|
||||||
return 'get';
|
$router1->post('/', fn() => 'post');
|
||||||
});
|
$router1->delete('/', fn() => 'delete');
|
||||||
$router1->post('/', function() {
|
$router1->patch('/', fn() => 'patch');
|
||||||
return 'post';
|
$router1->put('/', fn() => 'put');
|
||||||
});
|
$router1->add('custom', '/', fn() => 'wacky');
|
||||||
$router1->delete('/', function() {
|
|
||||||
return 'delete';
|
|
||||||
});
|
|
||||||
$router1->patch('/', function() {
|
|
||||||
return 'patch';
|
|
||||||
});
|
|
||||||
$router1->put('/', function() {
|
|
||||||
return 'put';
|
|
||||||
});
|
|
||||||
$router1->add('custom', '/', function() {
|
|
||||||
return 'wacky';
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->assertEquals(['get'], $router1->resolve('GET', '/')->runAll());
|
$this->assertEquals('get', $router1->resolve('GET', '/')->dispatch([]));
|
||||||
$this->assertEquals(['wacky'], $router1->resolve('CUSTOM', '/')->runAll());
|
$this->assertEquals('wacky', $router1->resolve('CUSTOM', '/')->dispatch([]));
|
||||||
|
|
||||||
$router1->use('/', function() {
|
$router1->use('/', function() { /* this one intentionally does nothing */ });
|
||||||
return 'warioware';
|
|
||||||
});
|
|
||||||
$router1->use('/deep', function() {
|
|
||||||
return 'deep';
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->assertEquals(['warioware', 'post'], $router1->resolve('POST', '/')->runAll());
|
// registration order should matter
|
||||||
|
$router1->use('/deep', fn() => 'deep');
|
||||||
|
|
||||||
$router1->use('/user/:user/below', function(string $user) {
|
$postRoot = $router1->resolve('POST', '/');
|
||||||
return 'warioware below ' . $user;
|
$this->assertNull($postRoot->runMiddleware([]));
|
||||||
});
|
$this->assertEquals('post', $postRoot->dispatch([]));
|
||||||
|
|
||||||
$router1->get('/user/static', function() {
|
$this->assertEquals('deep', $router1->resolve('GET', '/deep/nothing')->runMiddleware([]));
|
||||||
return 'the static one';
|
|
||||||
});
|
|
||||||
$router1->get('/user/static/below', function() {
|
|
||||||
return 'below the static one';
|
|
||||||
});
|
|
||||||
$router1->get('/user/:user', function(string $user) {
|
|
||||||
return $user;
|
|
||||||
});
|
|
||||||
$router1->get('/user/:user/below', function(string $user) {
|
|
||||||
return 'below ' . $user;
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->assertEquals(
|
$router1->use('/user/([A-Za-z0-9]+)/below', fn(string $user) => 'warioware below ' . $user);
|
||||||
['warioware', 'below the static one'],
|
|
||||||
$router1->resolve('GET', '/user/static/below')->runAll()
|
|
||||||
);
|
|
||||||
$this->assertEquals(
|
|
||||||
['warioware', 'warioware below flashwave', 'below flashwave'],
|
|
||||||
$router1->resolve('GET', '/user/flashwave/below')->runAll()
|
|
||||||
);
|
|
||||||
|
|
||||||
$router2 = new Router;
|
$router1->get('/user/static', fn() => 'the static one');
|
||||||
$router2->use('/', function() {
|
$router1->get('/user/static/below', fn() => 'below the static one');
|
||||||
return 'meow';
|
$router1->get('/user/([A-Za-z0-9]+)', fn(string $user) => $user);
|
||||||
});
|
$router1->get('/user/([A-Za-z0-9]+)/below', fn(string $user) => 'below ' . $user);
|
||||||
$router2->get('/rules', function() {
|
|
||||||
return 'rules page';
|
|
||||||
});
|
|
||||||
$router2->get('/contact', function() {
|
|
||||||
return 'contact page';
|
|
||||||
});
|
|
||||||
$router2->get('/25252', function() {
|
|
||||||
return 'numeric test';
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->assertEquals(['meow', 'rules page'], $router2->resolve('GET', '/rules')->runAll());
|
$this->assertEquals('below the static one', $router1->resolve('GET', '/user/static/below')->dispatch([]));
|
||||||
$this->assertEquals(['meow', 'numeric test'], $router2->resolve('GET', '/25252')->runAll());
|
|
||||||
|
|
||||||
$router3 = new Router;
|
$getWariowareBelowFlashwave = $router1->resolve('GET', '/user/flashwave/below');
|
||||||
$router3->get('/static', function() {
|
$this->assertEquals('warioware below flashwave', $getWariowareBelowFlashwave->runMiddleware([]));
|
||||||
return 'wrong';
|
$this->assertEquals('below flashwave', $getWariowareBelowFlashwave->dispatch([]));
|
||||||
});
|
|
||||||
$router3->get('/static/0', function() {
|
|
||||||
return 'correct';
|
|
||||||
});
|
|
||||||
$router3->get('/variable', function() {
|
|
||||||
return 'wrong';
|
|
||||||
});
|
|
||||||
$router3->get('/variable/:num', function(string $num) {
|
|
||||||
return $num === '0' ? 'correct' : 'VERY wrong';
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->assertEquals('correct', $router3->resolve('GET', '/static/0')->run());
|
$router2 = new HttpRouter;
|
||||||
$this->assertEquals('correct', $router3->resolve('GET', '/variable/0')->run());
|
$router2->use('/', fn() => 'meow');
|
||||||
|
$router2->get('/rules', fn() => 'rules page');
|
||||||
|
$router2->get('/contact', fn() => 'contact page');
|
||||||
|
$router2->get('/25252', fn() => 'numeric test');
|
||||||
|
|
||||||
|
$getRules = $router2->resolve('GET', '/rules');
|
||||||
|
$this->assertEquals('meow', $getRules->runMiddleware([]));
|
||||||
|
$this->assertEquals('rules page', $getRules->dispatch([]));
|
||||||
|
|
||||||
|
$get25252 = $router2->resolve('GET', '/25252');
|
||||||
|
$this->assertEquals('meow', $get25252->runMiddleware([]));
|
||||||
|
$this->assertEquals('numeric test', $get25252->dispatch([]));
|
||||||
|
|
||||||
|
$router3 = $router1->scopeTo('/scoped');
|
||||||
|
$router3->get('/static', fn() => 'wrong');
|
||||||
|
$router1->get('/scoped/static/0', fn() => 'correct');
|
||||||
|
$router3->get('/variable', fn() => 'wrong');
|
||||||
|
$router3->get('/variable/([0-9]+)', fn(string $num) => $num === '0' ? 'correct' : 'VERY wrong');
|
||||||
|
$router3->get('/variable/([a-z]+)', fn(string $char) => $char === 'a' ? 'correct' : 'VERY wrong');
|
||||||
|
|
||||||
|
$this->assertEquals('correct', $router3->resolve('GET', '/static/0')->dispatch([]));
|
||||||
|
$this->assertEquals('correct', $router1->resolve('GET', '/scoped/variable/0')->dispatch([]));
|
||||||
|
$this->assertEquals('correct', $router3->resolve('GET', '/variable/a')->dispatch([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAttribute(): void {
|
public function testAttribute(): void {
|
||||||
$router = new Router;
|
$router = new HttpRouter;
|
||||||
$handler = new class extends RouteHandler {
|
$handler = new class extends RouteHandler {
|
||||||
#[Route('GET', '/')]
|
#[HttpGet('/')]
|
||||||
public function getIndex() {
|
public function getIndex() {
|
||||||
return 'index';
|
return 'index';
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('POST', '/avatar')]
|
#[HttpPost('/avatar')]
|
||||||
public function postAvatar() {
|
public function postAvatar() {
|
||||||
return 'avatar';
|
return 'avatar';
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('PUT', '/static')]
|
#[HttpPut('/static')]
|
||||||
public static function putStatic() {
|
public static function putStatic() {
|
||||||
return 'static';
|
return 'static';
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('GET', '/meow')]
|
#[HttpGet('/meow')]
|
||||||
#[Route('POST', '/meow')]
|
#[HttpPost('/meow')]
|
||||||
public function multiple() {
|
public function multiple() {
|
||||||
return 'meow';
|
return 'meow';
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/mw')]
|
#[HttpMiddleware('/mw')]
|
||||||
public function useMw() {
|
public function useMw() {
|
||||||
return 'this intercepts';
|
return 'this intercepts';
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('GET', '/mw')]
|
#[HttpGet('/mw')]
|
||||||
public function getMw() {
|
public function getMw() {
|
||||||
return 'this is intercepted';
|
return 'this is intercepted';
|
||||||
}
|
}
|
||||||
|
@ -149,11 +120,31 @@ final class RouterTest extends TestCase {
|
||||||
};
|
};
|
||||||
|
|
||||||
$router->register($handler);
|
$router->register($handler);
|
||||||
$this->assertEquals('index', $router->resolve('GET', '/')->run());
|
$this->assertFalse($router->resolve('GET', '/soap')->hasHandler());
|
||||||
$this->assertEquals('avatar', $router->resolve('POST', '/avatar')->run());
|
|
||||||
$this->assertEquals('static', $router->resolve('PUT', '/static')->run());
|
$patchAvatar = $router->resolve('PATCH', '/avatar');
|
||||||
$this->assertEquals('meow', $router->resolve('GET', '/meow')->run());
|
$this->assertFalse($patchAvatar->hasHandler());
|
||||||
$this->assertEquals('meow', $router->resolve('POST', '/meow')->run());
|
$this->assertTrue($patchAvatar->hasOtherMethods());
|
||||||
$this->assertEquals('this intercepts', $router->resolve('GET', '/mw')->run());
|
$this->assertEquals(['POST'], $patchAvatar->getSupportedMethods());
|
||||||
|
|
||||||
|
$this->assertEquals('index', $router->resolve('GET', '/')->dispatch([]));
|
||||||
|
$this->assertEquals('avatar', $router->resolve('POST', '/avatar')->dispatch([]));
|
||||||
|
$this->assertEquals('static', $router->resolve('PUT', '/static')->dispatch([]));
|
||||||
|
$this->assertEquals('meow', $router->resolve('GET', '/meow')->dispatch([]));
|
||||||
|
$this->assertEquals('meow', $router->resolve('POST', '/meow')->dispatch([]));
|
||||||
|
|
||||||
|
// stopping on middleware is the dispatcher's job
|
||||||
|
$getMw = $router->resolve('GET', '/mw');
|
||||||
|
$this->assertEquals('this intercepts', $getMw->runMiddleware([]));
|
||||||
|
$this->assertEquals('this is intercepted', $getMw->dispatch([]));
|
||||||
|
|
||||||
|
$scoped = $router->scopeTo('/scoped');
|
||||||
|
$scoped->register($handler);
|
||||||
|
|
||||||
|
$this->assertEquals('index', $scoped->resolve('GET', '/')->dispatch([]));
|
||||||
|
$this->assertEquals('avatar', $router->resolve('POST', '/scoped/avatar')->dispatch([]));
|
||||||
|
$this->assertEquals('static', $scoped->resolve('PUT', '/static')->dispatch([]));
|
||||||
|
$this->assertEquals('meow', $router->resolve('GET', '/scoped/meow')->dispatch([]));
|
||||||
|
$this->assertEquals('meow', $scoped->resolve('POST', '/meow')->dispatch([]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue