diff --git a/VERSION b/VERSION
index fe1507c..6cefb80 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.2403.282326
+0.2403.301624
diff --git a/src/Http/HttpFx.php b/src/Http/HttpFx.php
deleted file mode 100644
index 23d4f2d..0000000
--- a/src/Http/HttpFx.php
+++ /dev/null
@@ -1,290 +0,0 @@
-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)) === 'setTypeHTML('utf-8');
- else {
- $charset = strtolower(mb_preferred_mime_name(mb_detect_encoding($result)));
-
- if(strtolower(substr($result, 0, 5)) === '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(
- '
%1$03d %2$s%1$03d %2$s
Index',
- $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);
- }
-}
diff --git a/src/Routing/IRouteHandler.php b/src/Routing/IRouteHandler.php
deleted file mode 100644
index adadb91..0000000
--- a/src/Routing/IRouteHandler.php
+++ /dev/null
@@ -1,18 +0,0 @@
-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);
- }
- }
- }
-}
diff --git a/src/Routing/RouteCallable.php b/src/Routing/RouteCallable.php
deleted file mode 100644
index dcbe073..0000000
--- a/src/Routing/RouteCallable.php
+++ /dev/null
@@ -1,72 +0,0 @@
-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;
- }
-}
diff --git a/src/Routing/RouteHandler.php b/src/Routing/RouteHandler.php
deleted file mode 100644
index 5b6ed52..0000000
--- a/src/Routing/RouteHandler.php
+++ /dev/null
@@ -1,14 +0,0 @@
-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)
- );
- }
-}
diff --git a/src/Routing/RouteMethodNotSupportedException.php b/src/Routing/RouteMethodNotSupportedException.php
deleted file mode 100644
index c9a6c61..0000000
--- a/src/Routing/RouteMethodNotSupportedException.php
+++ /dev/null
@@ -1,13 +0,0 @@
-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);
- }
-}
diff --git a/tests/RouterTest.php b/tests/RouterTest.php
index f88f3fb..d367efc 100644
--- a/tests/RouterTest.php
+++ b/tests/RouterTest.php
@@ -1,144 +1,115 @@
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([]));
}
}