Work in progress router rewrite.
This commit is contained in:
parent
73051dc71e
commit
1efb379601
18 changed files with 554 additions and 1 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2402.62138
|
||||
0.2403.281959
|
||||
|
|
48
src/Http/Routing/HandlerAttribute.php
Normal file
48
src/Http/Routing/HandlerAttribute.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
src/Http/Routing/HttpDelete.php
Normal file
15
src/Http/Routing/HttpDelete.php
Normal 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);
|
||||
}
|
||||
}
|
15
src/Http/Routing/HttpGet.php
Normal file
15
src/Http/Routing/HttpGet.php
Normal 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);
|
||||
}
|
||||
}
|
11
src/Http/Routing/HttpMiddleware.php
Normal file
11
src/Http/Routing/HttpMiddleware.php
Normal 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 {}
|
15
src/Http/Routing/HttpOptions.php
Normal file
15
src/Http/Routing/HttpOptions.php
Normal 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);
|
||||
}
|
||||
}
|
15
src/Http/Routing/HttpPatch.php
Normal file
15
src/Http/Routing/HttpPatch.php
Normal 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);
|
||||
}
|
||||
}
|
15
src/Http/Routing/HttpPost.php
Normal file
15
src/Http/Routing/HttpPost.php
Normal 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);
|
||||
}
|
||||
}
|
15
src/Http/Routing/HttpPut.php
Normal file
15
src/Http/Routing/HttpPut.php
Normal 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);
|
||||
}
|
||||
}
|
31
src/Http/Routing/HttpRoute.php
Normal file
31
src/Http/Routing/HttpRoute.php
Normal 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;
|
||||
}
|
||||
}
|
106
src/Http/Routing/HttpRouter.php
Normal file
106
src/Http/Routing/HttpRouter.php
Normal 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);
|
||||
}
|
||||
}
|
102
src/Http/Routing/IRouter.php
Normal file
102
src/Http/Routing/IRouter.php
Normal 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;
|
||||
}
|
18
src/Http/Routing/IRouterHandler.php
Normal file
18
src/Http/Routing/IRouterHandler.php
Normal 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;
|
||||
}
|
39
src/Http/Routing/ResolvedRouteInfo.php
Normal file
39
src/Http/Routing/ResolvedRouteInfo.php
Normal 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));
|
||||
}
|
||||
}
|
14
src/Http/Routing/RouteHandler.php
Normal file
14
src/Http/Routing/RouteHandler.php
Normal 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;
|
||||
}
|
16
src/Http/Routing/RouteHandlerTrait.php
Normal file
16
src/Http/Routing/RouteHandlerTrait.php
Normal 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);
|
||||
}
|
||||
}
|
36
src/Http/Routing/RouterTrait.php
Normal file
36
src/Http/Routing/RouterTrait.php
Normal 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);
|
||||
}
|
||||
}
|
42
src/Http/Routing/ScopedRouter.php
Normal file
42
src/Http/Routing/ScopedRouter.php
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue