Removed old router code.
This commit is contained in:
parent
9d5b050b89
commit
9b57fbb42c
13 changed files with 90 additions and 941 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2403.282326
|
||||
0.2403.301624
|
||||
|
|
|
@ -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
|
||||
// RouterTest.php
|
||||
// Created: 2022-01-20
|
||||
// Updated: 2023-09-11
|
||||
// Updated: 2024-03-30
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Index\Routing\Route;
|
||||
use Index\Routing\RouteHandler;
|
||||
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 {
|
||||
public function testRouter(): void {
|
||||
$router1 = new Router;
|
||||
$router1 = new HttpRouter;
|
||||
|
||||
$router1->get('/', function() {
|
||||
return 'get';
|
||||
});
|
||||
$router1->post('/', function() {
|
||||
return 'post';
|
||||
});
|
||||
$router1->delete('/', function() {
|
||||
return 'delete';
|
||||
});
|
||||
$router1->patch('/', function() {
|
||||
return 'patch';
|
||||
});
|
||||
$router1->put('/', function() {
|
||||
return 'put';
|
||||
});
|
||||
$router1->add('custom', '/', function() {
|
||||
return 'wacky';
|
||||
});
|
||||
$router1->get('/', fn() => 'get');
|
||||
$router1->post('/', fn() => 'post');
|
||||
$router1->delete('/', fn() => 'delete');
|
||||
$router1->patch('/', fn() => 'patch');
|
||||
$router1->put('/', fn() => 'put');
|
||||
$router1->add('custom', '/', fn() => 'wacky');
|
||||
|
||||
$this->assertEquals(['get'], $router1->resolve('GET', '/')->runAll());
|
||||
$this->assertEquals(['wacky'], $router1->resolve('CUSTOM', '/')->runAll());
|
||||
$this->assertEquals('get', $router1->resolve('GET', '/')->dispatch([]));
|
||||
$this->assertEquals('wacky', $router1->resolve('CUSTOM', '/')->dispatch([]));
|
||||
|
||||
$router1->use('/', function() {
|
||||
return 'warioware';
|
||||
});
|
||||
$router1->use('/deep', function() {
|
||||
return 'deep';
|
||||
});
|
||||
$router1->use('/', function() { /* this one intentionally does nothing */ });
|
||||
|
||||
$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) {
|
||||
return 'warioware below ' . $user;
|
||||
});
|
||||
$postRoot = $router1->resolve('POST', '/');
|
||||
$this->assertNull($postRoot->runMiddleware([]));
|
||||
$this->assertEquals('post', $postRoot->dispatch([]));
|
||||
|
||||
$router1->get('/user/static', function() {
|
||||
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('deep', $router1->resolve('GET', '/deep/nothing')->runMiddleware([]));
|
||||
|
||||
$this->assertEquals(
|
||||
['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()
|
||||
);
|
||||
$router1->use('/user/([A-Za-z0-9]+)/below', fn(string $user) => 'warioware below ' . $user);
|
||||
|
||||
$router2 = new Router;
|
||||
$router2->use('/', function() {
|
||||
return 'meow';
|
||||
});
|
||||
$router2->get('/rules', function() {
|
||||
return 'rules page';
|
||||
});
|
||||
$router2->get('/contact', function() {
|
||||
return 'contact page';
|
||||
});
|
||||
$router2->get('/25252', function() {
|
||||
return 'numeric test';
|
||||
});
|
||||
$router1->get('/user/static', fn() => 'the static one');
|
||||
$router1->get('/user/static/below', fn() => 'below the static one');
|
||||
$router1->get('/user/([A-Za-z0-9]+)', fn(string $user) => $user);
|
||||
$router1->get('/user/([A-Za-z0-9]+)/below', fn(string $user) => 'below ' . $user);
|
||||
|
||||
$this->assertEquals(['meow', 'rules page'], $router2->resolve('GET', '/rules')->runAll());
|
||||
$this->assertEquals(['meow', 'numeric test'], $router2->resolve('GET', '/25252')->runAll());
|
||||
$this->assertEquals('below the static one', $router1->resolve('GET', '/user/static/below')->dispatch([]));
|
||||
|
||||
$router3 = new Router;
|
||||
$router3->get('/static', function() {
|
||||
return 'wrong';
|
||||
});
|
||||
$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';
|
||||
});
|
||||
$getWariowareBelowFlashwave = $router1->resolve('GET', '/user/flashwave/below');
|
||||
$this->assertEquals('warioware below flashwave', $getWariowareBelowFlashwave->runMiddleware([]));
|
||||
$this->assertEquals('below flashwave', $getWariowareBelowFlashwave->dispatch([]));
|
||||
|
||||
$this->assertEquals('correct', $router3->resolve('GET', '/static/0')->run());
|
||||
$this->assertEquals('correct', $router3->resolve('GET', '/variable/0')->run());
|
||||
$router2 = new HttpRouter;
|
||||
$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 {
|
||||
$router = new Router;
|
||||
$router = new HttpRouter;
|
||||
$handler = new class extends RouteHandler {
|
||||
#[Route('GET', '/')]
|
||||
#[HttpGet('/')]
|
||||
public function getIndex() {
|
||||
return 'index';
|
||||
}
|
||||
|
||||
#[Route('POST', '/avatar')]
|
||||
#[HttpPost('/avatar')]
|
||||
public function postAvatar() {
|
||||
return 'avatar';
|
||||
}
|
||||
|
||||
#[Route('PUT', '/static')]
|
||||
#[HttpPut('/static')]
|
||||
public static function putStatic() {
|
||||
return 'static';
|
||||
}
|
||||
|
||||
#[Route('GET', '/meow')]
|
||||
#[Route('POST', '/meow')]
|
||||
#[HttpGet('/meow')]
|
||||
#[HttpPost('/meow')]
|
||||
public function multiple() {
|
||||
return 'meow';
|
||||
}
|
||||
|
||||
#[Route('/mw')]
|
||||
#[HttpMiddleware('/mw')]
|
||||
public function useMw() {
|
||||
return 'this intercepts';
|
||||
}
|
||||
|
||||
#[Route('GET', '/mw')]
|
||||
#[HttpGet('/mw')]
|
||||
public function getMw() {
|
||||
return 'this is intercepted';
|
||||
}
|
||||
|
@ -149,11 +120,31 @@ final class RouterTest extends TestCase {
|
|||
};
|
||||
|
||||
$router->register($handler);
|
||||
$this->assertEquals('index', $router->resolve('GET', '/')->run());
|
||||
$this->assertEquals('avatar', $router->resolve('POST', '/avatar')->run());
|
||||
$this->assertEquals('static', $router->resolve('PUT', '/static')->run());
|
||||
$this->assertEquals('meow', $router->resolve('GET', '/meow')->run());
|
||||
$this->assertEquals('meow', $router->resolve('POST', '/meow')->run());
|
||||
$this->assertEquals('this intercepts', $router->resolve('GET', '/mw')->run());
|
||||
$this->assertFalse($router->resolve('GET', '/soap')->hasHandler());
|
||||
|
||||
$patchAvatar = $router->resolve('PATCH', '/avatar');
|
||||
$this->assertFalse($patchAvatar->hasHandler());
|
||||
$this->assertTrue($patchAvatar->hasOtherMethods());
|
||||
$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