Work in progress router rewrite.

This commit is contained in:
Pachira 2024-03-28 20:01:34 +00:00
parent 73051dc71e
commit 1efb379601
18 changed files with 554 additions and 1 deletions

View file

@ -1 +1 @@
0.2402.62138 0.2403.281959

View file

@ -0,0 +1,48 @@
<?php
// HandlerAttribute.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
/**
* Provides an attribute for marking methods in a class as handlers.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
abstract class HandlerAttribute {
public function __construct(private string $path) {}
/**
* 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 register(IRouter $router, IRouteHandler $handler): void {
$objectInfo = new ReflectionObject($handler);
$methodInfos = $objectInfo->getMethods();
foreach($methodInfos as $methodInfo) {
$attrInfos = $methodInfo->getAttributes(HandlerAttribute::class);
foreach($attrInfos as $attrInfo) {
$handlerInfo = $attrInfo->newInstance();
$closure = $methodInfo->getClosure($methodInfo->isStatic() ? null : $handler);
if($handlerInfo instanceof HttpRoute)
$router->add($handlerInfo->getMethod(), $handlerInfo->getPath(), $closure);
else
$router->use($handlerInfo->getPath(), $closure);
}
}
}
}

View file

@ -0,0 +1,15 @@
<?php
// HttpDelete.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
/**
* Provides an attribute for marking methods in a class as a DELETE route.
*/
class HttpDelete extends HttpRoute {
public function __construct(string $path) {
parent::__construct('DELETE', $path);
}
}

View file

@ -0,0 +1,15 @@
<?php
// HttpGet.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
/**
* Provides an attribute for marking methods in a class as a GET route.
*/
class HttpGet extends HttpRoute {
public function __construct(string $path) {
parent::__construct('GET', $path);
}
}

View file

@ -0,0 +1,11 @@
<?php
// HttpMiddleware.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
/**
* Provides an attribute for marking methods in a class as middleware.
*/
class HttpMiddleware extends HandlerAttribute {}

View file

@ -0,0 +1,15 @@
<?php
// HttpOptions.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
/**
* Provides an attribute for marking methods in a class as a OPTIONS route.
*/
class HttpOptions extends HttpRoute {
public function __construct(string $path) {
parent::__construct('OPTIONS', $path);
}
}

View file

@ -0,0 +1,15 @@
<?php
// HttpPatch.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
/**
* Provides an attribute for marking methods in a class as a POST route.
*/
class HttpPatch extends HttpRoute {
public function __construct(string $path) {
parent::__construct('PATCH', $path);
}
}

View file

@ -0,0 +1,15 @@
<?php
// HttpPost.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
/**
* Provides an attribute for marking methods in a class as a POST route.
*/
class HttpPost extends HttpRoute {
public function __construct(string $path) {
parent::__construct('POST', $path);
}
}

View file

@ -0,0 +1,15 @@
<?php
// HttpPut.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
/**
* Provides an attribute for marking methods in a class as a PUT route.
*/
class HttpPut extends HttpRoute {
public function __construct(string $path) {
parent::__construct('PUT', $path);
}
}

View file

@ -0,0 +1,31 @@
<?php
// HttpRoute.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
/**
* Provides an attribute for marking methods in a class as a route.
*/
class HttpRoute extends HandlerAttribute {
private string $method;
/**
* @param string $method
* @param string $path
*/
public function __construct(string $method, string $path) {
parent::__construct($path);
$this->method = $method;
}
/**
* Returns the target method name.
*
* @return string
*/
public function getMethod(): string {
return $this->method;
}
}

View file

@ -0,0 +1,106 @@
<?php
// HttpRouter.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
use stdClass;
use InvalidArgumentException;
use RuntimeException;
class HttpRouter implements IRouter {
use RouterTrait;
private array $middlewares = [];
private array $staticRoutes = [];
private array $dynamicRoutes = [];
public function scopeTo(string $prefix): IRouter {
return new ScopedRouter($this, $prefix);
}
private static function preparePath(string $path, bool $prefixMatch): string|false {
// this sucks lol
if(!str_contains($path, '(') || !str_contains($path, ')'))
return false;
return sprintf('#^%s%s#su', $path, $prefixMatch ? '' : '$');
}
public function use(string $path, callable $handler): void {
$this->middlewares[] = $mwInfo = new stdClass;
$mwInfo->handler = $handler;
$prepared = self::preparePath($path, true);
$mwInfo->dynamic = $prepared !== false;
if($mwInfo->dynamic)
$mwInfo->match = $prepared;
else
$mwInfo->prefix = $path;
}
public function add(string $method, string $path, callable $handler): void {
if($method === '')
throw new InvalidArgumentException('$method may not be empty');
$method = strtoupper($method);
if(trim($method) !== $method)
throw new InvalidArgumentException('$method may start or end with whitespace');
$prepared = self::preparePath($path, false);
if($prepared === false) {
if(is_array($this->staticRoutes[$path]))
$this->staticRoutes[$path][$method] = $handler;
else
$this->staticRoutes[$path] = [$method => $handler];
} else {
if(array_key_exists($prepared, $this->dynamicRoutes))
$this->dynamicRoutes[$prepared][$method] = $handler;
else
$this->dynamicRoutes[$prepared] = [$method => $handler];
}
}
public function resolve(string $method, string $path): ResolvedRouteInfo {
$middlewares = [];
foreach($this->middlewares as $mwInfo) {
if($mwInfo->dynamic) {
if(preg_match($mwInfo->match, $path, $args) !== 1)
continue;
array_shift($args);
} else {
if(!str_starts_with($path, $mwInfo->prefix))
continue;
$args = [];
}
$middlewares[] = [$mwInfo->handler, $args];
}
$methods = [];
$handler = null;
$args = [];
if(array_key_exists($path, $this->staticRoutes)) {
$methods = $this->staticRoutes[$path];
} else {
foreach($this->dynamicRoutes as $rPattern => $rMethods)
if(preg_match($rPattern, $path, $args) === 1) {
$methods = $rMethods;
break;
}
}
$method = strtoupper($method);
if(array_key_exists($method, $methods))
$handler = $methods[$method];
return new ResolvedRouteInfo($middlewares, array_keys($methods), $handler, $args);
}
}

View file

@ -0,0 +1,102 @@
<?php
// IRouter.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
use InvalidArgumentException;
use RuntimeException;
use Index\Http\HttpRequest;
interface IRouter {
/**
* Creates a scoped version of this router.
*
* @param string $prefix Prefix path to prepend to all registered routes.
* @return IRouter Scoped router.
*/
public function scopeTo(string $prefix): 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.
* @throws InvalidArgumentException if $method is not a valid method name
*/
public function add(string $method, string $path, callable $handler): void;
/**
* Resolves a route
*
* @param string $method Request method.
* @param string $path Request path.
* @return ResolvedRouteInfo Response route.
*/
public function resolve(string $method, string $path): ResolvedRouteInfo;
/**
* 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;
}

View file

@ -0,0 +1,18 @@
<?php
// IRouterHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\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;
}

View file

@ -0,0 +1,39 @@
<?php
// ResolvedRouteInfo.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
class ResolvedRouteInfo {
public function __construct(
private array $middlewares,
private array $supportedMethods,
private ?callable $handler,
private array $args,
) {}
public function runMiddleware(array $args): mixed {
foreach($this->middlewares as $middleware) {
$result = $middleware[0](...array_merge($args, $middleware[1]));
if($result !== null)
return $result;
}
}
public function hasHandler(): bool {
return $this->handler !== null;
}
public function hasOtherMethods(): bool {
return !empty($this->supportedMethods);
}
public function getSupportedMethods(): array {
return $this->supportedMethods;
}
public function dispatch(array $args): mixed {
return $this->handler(...array_merge($args, $this->args));
}
}

View file

@ -0,0 +1,14 @@
<?php
// RouteHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\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;
}

View file

@ -0,0 +1,16 @@
<?php
// RouteHandlerTrait.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
/**
* Provides an implementation of IRouteHandler::registerRoutes that uses the attributes.
* For more advanced use, everything can be use'd separately and HandlerAttribute::register called manually.
*/
trait RouteHandlerTrait {
public function registerRoutes(IRouter $router): void {
HandlerAttribute::register($router, $this);
}
}

View file

@ -0,0 +1,36 @@
<?php
// RouterTrait.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
trait RouterTrait {
public function get(string $path, callable $handler): void {
$this->add('get', $path, $handler);
}
public function post(string $path, callable $handler): void {
$this->add('post', $path, $handler);
}
public function delete(string $path, callable $handler): void {
$this->add('delete', $path, $handler);
}
public function patch(string $path, callable $handler): void {
$this->add('patch', $path, $handler);
}
public function put(string $path, callable $handler): void {
$this->add('put', $path, $handler);
}
public function options(string $path, callable $handler): void {
$this->add('options', $path, $handler);
}
public function register(IRouteHandler $handler): void {
$handler->registerRoutes($this);
}
}

View file

@ -0,0 +1,42 @@
<?php
// ScopedRouter.php
// Created: 2024-03-28
// Updated: 2024-03-28
namespace Index\Http\Routing;
class ScopedRouter implements IRouter {
use RouterTrait;
private IRouter $router;
private string $prefix;
public function __construct(IRouter $router, string $prefix) {
if($router instanceof ScopedRouter)
$router = $router->getParentRouter();
$this->router = $router;
// todo: cleanup
$this->prefix = $prefix;
}
private function getParentRouter(): IRouter {
return $this->router;
}
public function scopeTo(string $prefix): IRouter {
return $this->router->scopeTo($this->prefix . $prefix);
}
public function add(string $method, string $path, callable $handler): void {
$this->router->add($method, $this->prefix . $path, $handler);
}
public function use(string $path, callable $handler): void {
$this->router->use($this->prefix . $path, $handler);
}
public function resolve(string $method, string $path): ResolvedRouteInfo {
return $this->router->resolve($method, $this->prefix . $path);
}
}