From f543d74bfeb8b6c369b1533592a166c3a47bba97 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Fri, 12 Jan 2024 11:05:24 -0700 Subject: [PATCH 1/3] Middleware code --- flight/Engine.php | 71 ++++++++++++++++++++++++++++++++++++----- flight/Flight.php | 11 ++++--- flight/net/Route.php | 30 ++++++++++++++++++ flight/net/Router.php | 60 +++++++++++++++++++++++------------ tests/EngineTest.php | 73 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 212 insertions(+), 33 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index 44affc1f..cacd19ec 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -10,6 +10,7 @@ namespace flight; +use Closure; use ErrorException; use Exception; use flight\core\Dispatcher; @@ -19,6 +20,7 @@ use flight\net\Router; use flight\template\View; use Throwable; +use flight\net\Route; /** * The Engine class contains the core functionality of the framework. @@ -32,12 +34,12 @@ * @method void halt(int $code = 200, string $message = '') Stops processing and returns a given response. * * Routing - * @method void route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a URL to a callback function with all applicable methods + * @method Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a URL to a callback function with all applicable methods * @method void group(string $pattern, callable $callback) Groups a set of routes together under a common prefix. - * @method void post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a POST URL to a callback function. - * @method void put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PUT URL to a callback function. - * @method void patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PATCH URL to a callback function. - * @method void delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a DELETE URL to a callback function. + * @method Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a POST URL to a callback function. + * @method Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PUT URL to a callback function. + * @method Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PATCH URL to a callback function. + * @method Route delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a DELETE URL to a callback function. * @method Router router() Gets router * @method string getUrl(string $alias) Gets a url from an alias * @@ -375,6 +377,7 @@ public function _start(): void ob_start(); // Route the request + $failed_middleware_check = false; while ($route = $router->route($request)) { $params = array_values($route->params); @@ -383,11 +386,60 @@ public function _start(): void $params[] = $route; } + // Run any before middlewares + if(count($route->middleware) > 0) { + foreach($route->middleware as $middleware) { + + $middleware_object = (is_callable($middleware) === true ? $middleware : (method_exists($middleware, 'before') === true ? [ $middleware, 'before' ]: false)); + + if($middleware_object === false) { + continue; + } + + // It's assumed if you don't declare before, that it will be assumed as the before method + $middleware_result = $this->dispatcher->execute( + $middleware_object, + $params + ); + + if ($middleware_result === false) { + $failed_middleware_check = true; + break 2; + } + } + } + // Call route handler $continue = $this->dispatcher->execute( $route->callback, $params ); + + + // Run any before middlewares + if(count($route->middleware) > 0) { + + // process the middleware in reverse order now + foreach(array_reverse($route->middleware) as $middleware) { + + // must be an object. No functions allowed here + $middleware_object = is_object($middleware) === true && !($middleware instanceof Closure) && method_exists($middleware, 'after') === true ? [ $middleware, 'after' ] : false; + + // has to have the after method, otherwise just skip it + if($middleware_object === false) { + continue; + } + + $middleware_result = $this->dispatcher->execute( + $middleware_object, + $params + ); + if ($middleware_result === false) { + $failed_middleware_check = true; + break 2; + } + } + } $dispatched = true; @@ -400,7 +452,9 @@ public function _start(): void $dispatched = false; } - if (!$dispatched) { + if($failed_middleware_check === true) { + $this->halt(403, 'Forbidden'); + } else if($dispatched === false) { $this->notFound(); } } @@ -464,10 +518,11 @@ public function _stop(?int $code = null): void * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias the alias for the route + * @return Route */ - public function _route(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): void + public function _route(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route { - $this->router()->map($pattern, $callback, $pass_route, $alias); + return $this->router()->map($pattern, $callback, $pass_route, $alias); } /** diff --git a/flight/Flight.php b/flight/Flight.php index fd3772c6..c5b61e04 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -14,6 +14,7 @@ use flight\net\Response; use flight\net\Router; use flight\template\View; +use flight\net\Route; /** * The Flight class is a static representation of the framework. @@ -23,12 +24,12 @@ * @method static void stop() Stops the framework and sends a response. * @method static void halt(int $code = 200, string $message = '') Stop the framework with an optional status code and message. * - * @method static void route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Maps a URL pattern to a callback with all applicable methods. + * @method static Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Maps a URL pattern to a callback with all applicable methods. * @method static void group(string $pattern, callable $callback) Groups a set of routes together under a common prefix. - * @method static void post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a POST URL to a callback function. - * @method static void put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PUT URL to a callback function. - * @method static void patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PATCH URL to a callback function. - * @method static void delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a DELETE URL to a callback function. + * @method static Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a POST URL to a callback function. + * @method static Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PUT URL to a callback function. + * @method static Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PATCH URL to a callback function. + * @method static Route delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a DELETE URL to a callback function. * @method static Router router() Returns Router instance. * @method static string getUrl(string $alias) Gets a url from an alias * diff --git a/flight/net/Route.php b/flight/net/Route.php index 63b1bf05..5f453925 100644 --- a/flight/net/Route.php +++ b/flight/net/Route.php @@ -57,6 +57,11 @@ final class Route */ public string $alias = ''; + /** + * @var array The middleware to be applied to the route + */ + public array $middleware = []; + /** * Constructor. * @@ -190,4 +195,29 @@ public function hydrateUrl(array $params = []): string { $url = rtrim($url, '/'); return $url; } + + /** + * Sets the route alias + * + * @return self + */ + public function setAlias(string $alias): self { + $this->alias = $alias; + return $this; + } + + /** + * Sets the route middleware + * + * @param array|callable $middleware + * @return self + */ + public function addMiddleware($middleware): self { + if(is_array($middleware) === true) { + $this->middleware = array_merge($this->middleware, $middleware); + } else { + $this->middleware[] = $middleware; + } + return $this; + } } diff --git a/flight/net/Router.php b/flight/net/Router.php index f8256165..cd49c9a7 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -26,7 +26,7 @@ class Router public bool $case_sensitive = false; /** * Mapped routes. - * @var array + * @var array */ protected array $routes = []; @@ -42,6 +42,13 @@ class Router */ protected string $group_prefix = ''; + /** + * Group Middleware + * + * @var array + */ + protected array $group_middlewares = []; + /** * Gets mapped routes. * @@ -67,9 +74,9 @@ public function clear(): void * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $route_alias Alias for the route - * @return void + * @return Route */ - public function map(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void + public function map(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): Route { $url = trim($pattern); $methods = ['*']; @@ -80,7 +87,16 @@ public function map(string $pattern, callable $callback, bool $pass_route = fals $methods = explode('|', $method); } - $this->routes[] = new Route($this->group_prefix.$url, $callback, $methods, $pass_route, $route_alias); + $route = new Route($this->group_prefix.$url, $callback, $methods, $pass_route, $route_alias); + + // to handle group middleware + foreach($this->group_middlewares as $gm) { + $route->addMiddleware($gm); + } + + $this->routes[] = $route; + + return $route; } /** @@ -90,10 +106,10 @@ public function map(string $pattern, callable $callback, bool $pass_route = fals * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route - * @return void + * @return Route */ - public function get(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): void { - $this->map('GET ' . $pattern, $callback, $pass_route, $alias); + public function get(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route { + return $this->map('GET ' . $pattern, $callback, $pass_route, $alias); } /** @@ -103,10 +119,10 @@ public function get(string $pattern, callable $callback, bool $pass_route = fals * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route - * @return void + * @return Route */ - public function post(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): void { - $this->map('POST ' . $pattern, $callback, $pass_route, $alias); + public function post(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route { + return $this->map('POST ' . $pattern, $callback, $pass_route, $alias); } /** @@ -116,10 +132,10 @@ public function post(string $pattern, callable $callback, bool $pass_route = fal * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route - * @return void + * @return Route */ - public function put(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): void { - $this->map('PUT ' . $pattern, $callback, $pass_route, $alias); + public function put(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route { + return $this->map('PUT ' . $pattern, $callback, $pass_route, $alias); } /** @@ -129,10 +145,10 @@ public function put(string $pattern, callable $callback, bool $pass_route = fals * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route - * @return void + * @return Route */ - public function patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): void { - $this->map('PATCH ' . $pattern, $callback, $pass_route, $alias); + public function patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route { + return $this->map('PATCH ' . $pattern, $callback, $pass_route, $alias); } /** @@ -142,10 +158,10 @@ public function patch(string $pattern, callable $callback, bool $pass_route = fa * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route - * @return void + * @return Route */ - public function delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): void { - $this->map('DELETE ' . $pattern, $callback, $pass_route, $alias); + public function delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route { + return $this->map('DELETE ' . $pattern, $callback, $pass_route, $alias); } /** @@ -153,13 +169,17 @@ public function delete(string $pattern, callable $callback, bool $pass_route = f * * @param string $group_prefix group URL prefix (such as /api/v1) * @param callable $callback The necessary calling that holds the Router class + * @param array $middlewares The middlewares to be applied to the group Ex: [ $middleware1, $middleware2 ] * @return void */ - public function group(string $group_prefix, callable $callback): void { + public function group(string $group_prefix, callable $callback, array $group_middlewares = []): void { $old_group_prefix = $this->group_prefix; + $old_group_middlewares = $this->group_middlewares; $this->group_prefix .= $group_prefix; + $this->group_middlewares = array_merge($this->group_middlewares, $group_middlewares); $callback($this); $this->group_prefix = $old_group_prefix; + $this->group_middlewares = $old_group_middlewares; } /** diff --git a/tests/EngineTest.php b/tests/EngineTest.php index e1b9aaf1..8c0697ec 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -272,4 +272,77 @@ public function testGetUrl() { $url = $engine->getUrl('path1', [ 'param' => 123 ]); $this->assertEquals('/path1/123', $url); } + + public function testMiddlewareCallableFunction() { + $engine = new Engine(); + $engine->route('/path1/@id', function($id) { echo 'OK'.$id; }) + ->addMiddleware(function($id) { echo 'before'.$id; }); + $engine->request()->url = '/path1/123'; + $engine->start(); + $this->expectOutputString('before123OK123'); + } + + public function testMiddlewareCallableFunctionReturnFalse() { + $engine = new class extends Engine { + public function _halt(int $code = 200, string $message = ''): void + { + $this->response()->status($code); + $this->response()->write($message); + } + }; + $engine->route('/path1/@id', function($id) { echo 'OK'.$id; }) + ->addMiddleware(function($id) { echo 'before'.$id; return false; }); + $engine->request()->url = '/path1/123'; + $engine->start(); + $this->expectOutputString('Forbiddenbefore123'); + $this->assertEquals(403, $engine->response()->status()); + } + + public function testMiddlewareClassBefore() { + $middleware = new class { + public function before($id) { + echo 'before'.$id; + } + }; + $engine = new Engine(); + + $engine->route('/path1/@id', function($id) { echo 'OK'.$id; }) + ->addMiddleware($middleware); + $engine->request()->url = '/path1/123'; + $engine->start(); + $this->expectOutputString('before123OK123'); + } + + public function testMiddlewareClassBeforeAndAfter() { + $middleware = new class { + public function before($id) { + echo 'before'.$id; + } + public function after($id) { + echo 'after'.$id; + } + }; + $engine = new Engine(); + + $engine->route('/path1/@id', function($id) { echo 'OK'.$id; }) + ->addMiddleware($middleware); + $engine->request()->url = '/path1/123'; + $engine->start(); + $this->expectOutputString('before123OK123after123'); + } + + public function testMiddlewareClassAfter() { + $middleware = new class { + public function after($id) { + echo 'after'.$id; + } + }; + $engine = new Engine(); + + $engine->route('/path1/@id', function($id) { echo 'OK'.$id; }) + ->addMiddleware($middleware); + $engine->request()->url = '/path1/123'; + $engine->start(); + $this->expectOutputString('OK123after123'); + } } From c98d6ce7d484e22e22991160272148bcff4e2f27 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Sat, 13 Jan 2024 13:42:06 -0700 Subject: [PATCH 2/3] Lots of tweaks for middleware. Cleaned up old code. --- flight/Engine.php | 22 ++++---- flight/Flight.php | 2 +- flight/core/Dispatcher.php | 61 +++------------------ flight/core/Loader.php | 25 +-------- tests/EngineTest.php | 108 +++++++++++++++++++++++++++++++++---- tests/LoaderTest.php | 6 --- tests/RouterTest.php | 12 +++++ 7 files changed, 128 insertions(+), 108 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index cacd19ec..4f0f60a6 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -35,7 +35,7 @@ * * Routing * @method Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a URL to a callback function with all applicable methods - * @method void group(string $pattern, callable $callback) Groups a set of routes together under a common prefix. + * @method void group(string $pattern, callable $callback, array $group_middlewares = []) Groups a set of routes together under a common prefix. * @method Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a POST URL to a callback function. * @method Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PUT URL to a callback function. * @method Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PATCH URL to a callback function. @@ -397,10 +397,7 @@ public function _start(): void } // It's assumed if you don't declare before, that it will be assumed as the before method - $middleware_result = $this->dispatcher->execute( - $middleware_object, - $params - ); + $middleware_result = $middleware_object($route->params); if ($middleware_result === false) { $failed_middleware_check = true; @@ -430,10 +427,8 @@ public function _start(): void continue; } - $middleware_result = $this->dispatcher->execute( - $middleware_object, - $params - ); + $middleware_result = $middleware_object($route->params); + if ($middleware_result === false) { $failed_middleware_check = true; break 2; @@ -528,12 +523,13 @@ public function _route(string $pattern, callable $callback, bool $pass_route = f /** * Routes a URL to a callback function. * - * @param string $pattern URL pattern to match - * @param callable $callback Callback function that includes the Router class as first parameter + * @param string $pattern URL pattern to match + * @param callable $callback Callback function that includes the Router class as first parameter + * @param array $group_middlewares The middleware to be applied to the route */ - public function _group(string $pattern, callable $callback): void + public function _group(string $pattern, callable $callback, array $group_middlewares = []): void { - $this->router()->group($pattern, $callback); + $this->router()->group($pattern, $callback, $group_middlewares); } /** diff --git a/flight/Flight.php b/flight/Flight.php index c5b61e04..a24a8f36 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -25,7 +25,7 @@ * @method static void halt(int $code = 200, string $message = '') Stop the framework with an optional status code and message. * * @method static Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Maps a URL pattern to a callback with all applicable methods. - * @method static void group(string $pattern, callable $callback) Groups a set of routes together under a common prefix. + * @method static void group(string $pattern, callable $callback, array $group_middlewares = []) Groups a set of routes together under a common prefix. * @method static Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a POST URL to a callback function. * @method static Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PUT URL to a callback function. * @method static Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PATCH URL to a callback function. diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 4ce68540..e31d3f98 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -53,7 +53,8 @@ final public function run(string $name, array $params = []) } // Run requested method - $output = self::execute($this->get($name), $params); + $callback = $this->get($name); + $output = $callback(...$params); // Run post-filters if (!empty($this->filters[$name]['after'])) { @@ -140,7 +141,7 @@ final public function filter(array $filters, array &$params, &$output): void { $args = [&$params, &$output]; foreach ($filters as $callback) { - $continue = self::execute($callback, $args); + $continue = $callback(...$args); if (false === $continue) { break; } @@ -178,27 +179,7 @@ public static function execute($callback, array &$params = []) */ public static function callFunction($func, array &$params = []) { - // Call static method - if (\is_string($func) && false !== strpos($func, '::')) { - return \call_user_func_array($func, $params); - } - - switch (\count($params)) { - case 0: - return $func(); - case 1: - return $func($params[0]); - case 2: - return $func($params[0], $params[1]); - case 3: - return $func($params[0], $params[1], $params[2]); - case 4: - return $func($params[0], $params[1], $params[2], $params[3]); - case 5: - return $func($params[0], $params[1], $params[2], $params[3], $params[4]); - default: - return \call_user_func_array($func, $params); - } + return call_user_func_array($func, $params); } /** @@ -215,37 +196,9 @@ public static function invokeMethod($func, array &$params = []) $instance = \is_object($class); - switch (\count($params)) { - case 0: - return ($instance) ? - $class->$method() : - $class::$method(); - case 1: - return ($instance) ? - $class->$method($params[0]) : - $class::$method($params[0]); - case 2: - return ($instance) ? - $class->$method($params[0], $params[1]) : - $class::$method($params[0], $params[1]); - case 3: - return ($instance) ? - $class->$method($params[0], $params[1], $params[2]) : - $class::$method($params[0], $params[1], $params[2]); - // This will be refactored soon enough - // @codeCoverageIgnoreStart - case 4: - return ($instance) ? - $class->$method($params[0], $params[1], $params[2], $params[3]) : - $class::$method($params[0], $params[1], $params[2], $params[3]); - case 5: - return ($instance) ? - $class->$method($params[0], $params[1], $params[2], $params[3], $params[4]) : - $class::$method($params[0], $params[1], $params[2], $params[3], $params[4]); - default: - return \call_user_func_array($func, $params); - // @codeCoverageIgnoreEnd - } + return ($instance) ? + $class->$method(...$params) : + $class::$method(); } /** diff --git a/flight/core/Loader.php b/flight/core/Loader.php index d262e742..0a64fb62 100644 --- a/flight/core/Loader.php +++ b/flight/core/Loader.php @@ -136,30 +136,7 @@ public function newInstance($class, array $params = []) return \call_user_func_array($class, $params); } - switch (\count($params)) { - case 0: - return new $class(); - case 1: - return new $class($params[0]); - // @codeCoverageIgnoreStart - case 2: - return new $class($params[0], $params[1]); - case 3: - return new $class($params[0], $params[1], $params[2]); - case 4: - return new $class($params[0], $params[1], $params[2], $params[3]); - case 5: - return new $class($params[0], $params[1], $params[2], $params[3], $params[4]); - // @codeCoverageIgnoreEnd - default: - try { - $refClass = new ReflectionClass($class); - - return $refClass->newInstanceArgs($params); - } catch (ReflectionException $e) { - throw new Exception("Cannot instantiate {$class}", 0, $e); - } - } + return new $class(...$params); } /** diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 8c0697ec..62fbf706 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -276,7 +276,7 @@ public function testGetUrl() { public function testMiddlewareCallableFunction() { $engine = new Engine(); $engine->route('/path1/@id', function($id) { echo 'OK'.$id; }) - ->addMiddleware(function($id) { echo 'before'.$id; }); + ->addMiddleware(function($params) { echo 'before'.$params['id']; }); $engine->request()->url = '/path1/123'; $engine->start(); $this->expectOutputString('before123OK123'); @@ -291,7 +291,7 @@ public function _halt(int $code = 200, string $message = ''): void } }; $engine->route('/path1/@id', function($id) { echo 'OK'.$id; }) - ->addMiddleware(function($id) { echo 'before'.$id; return false; }); + ->addMiddleware(function($params) { echo 'before'.$params['id']; return false; }); $engine->request()->url = '/path1/123'; $engine->start(); $this->expectOutputString('Forbiddenbefore123'); @@ -300,8 +300,8 @@ public function _halt(int $code = 200, string $message = ''): void public function testMiddlewareClassBefore() { $middleware = new class { - public function before($id) { - echo 'before'.$id; + public function before($params) { + echo 'before'.$params['id']; } }; $engine = new Engine(); @@ -315,11 +315,11 @@ public function before($id) { public function testMiddlewareClassBeforeAndAfter() { $middleware = new class { - public function before($id) { - echo 'before'.$id; + public function before($params) { + echo 'before'.$params['id']; } - public function after($id) { - echo 'after'.$id; + public function after($params) { + echo 'after'.$params['id']; } }; $engine = new Engine(); @@ -333,8 +333,9 @@ public function after($id) { public function testMiddlewareClassAfter() { $middleware = new class { - public function after($id) { - echo 'after'.$id; + public function after($params) { + + echo 'after'.$params['id']; } }; $engine = new Engine(); @@ -345,4 +346,91 @@ public function after($id) { $engine->start(); $this->expectOutputString('OK123after123'); } + + public function testMiddlewareClassAfterFailedCheck() { + $middleware = new class { + public function after($params) { + echo 'after'.$params['id']; + return false; + } + }; + $engine = new class extends Engine { + public function _halt(int $code = 200, string $message = ''): void + { + $this->response()->status($code); + $this->response()->write($message); + } + }; + + $engine->route('/path1/@id', function($id) { echo 'OK'.$id; }) + ->addMiddleware($middleware); + $engine->request()->url = '/path1/123'; + $engine->start(); + $this->assertEquals(403, $engine->response()->status()); + $this->expectOutputString('ForbiddenOK123after123'); + } + + public function testMiddlewareCallableFunctionMultiple() { + $engine = new Engine(); + $engine->route('/path1/@id', function($id) { echo 'OK'.$id; }) + ->addMiddleware(function($params) { echo 'before1'.$params['id']; }) + ->addMiddleware(function($params) { echo 'before2'.$params['id']; }); + $engine->request()->url = '/path1/123'; + $engine->start(); + $this->expectOutputString('before1123before2123OK123'); + } + + // Pay attention to the order on how the middleware is executed in this test. + public function testMiddlewareClassCallableRouteMultiple() { + $middleware = new class { + public function before($params) { + echo 'before'.$params['another_id']; + } + public function after($params) { + echo 'after'.$params['id']; + } + }; + $middleware2 = new class { + public function before($params) { + echo 'before'.$params['id']; + } + public function after($params) { + echo 'after'.$params['id'].$params['another_id']; + } + }; + $engine = new Engine(); + $engine->route('/path1/@id/subpath1/@another_id', function() { echo 'OK'; })->addMiddleware([ $middleware, $middleware2 ]); + + $engine->request()->url = '/path1/123/subpath1/456'; + $engine->start(); + $this->expectOutputString('before456before123OKafter123456after123'); + } + + public function testMiddlewareClassGroupRouteMultipleBooyah() { + $middleware = new class { + public function before($params) { + echo 'before'.$params['another_id']; + } + public function after($params) { + echo 'after'.$params['id']; + } + }; + $middleware2 = new class { + public function before($params) { + echo 'before'.$params['id']; + } + public function after($params) { + echo 'after'.$params['id'].$params['another_id']; + } + }; + $engine = new Engine(); + $engine->group('/path1/@id', function($router) { + $router->map('/subpath1/@another_id', function() { echo 'OK'; }); + $router->map('/@cool_id', function() { echo 'OK'; }); + }, [ $middleware, $middleware2 ]); + + $engine->request()->url = '/path1/123/subpath1/456'; + $engine->start(); + $this->expectOutputString('before456before123OKafter123456after123'); + } } diff --git a/tests/LoaderTest.php b/tests/LoaderTest.php index ecadd0a5..6c89a823 100644 --- a/tests/LoaderTest.php +++ b/tests/LoaderTest.php @@ -138,12 +138,6 @@ public function testNewInstance6Params() { $this->assertEquals('Suzie', $TesterClass->param6); } - public function testNewInstance6ParamsBadClass() { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Cannot instantiate BadClass'); - $TesterClass = $this->loader->newInstance('BadClass', ['Bob','Fred', 'Joe', 'Jane', 'Sally', 'Suzie']); - } - public function testAddDirectoryAsArray() { $loader = new class extends Loader { public function getDirectories() { diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 6125f53d..9fa77f2e 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -298,6 +298,18 @@ public function testRouteWithParameters() $this->check(); } + public function testRouteBeingReturned() { + $route = $this->router->map('/hi', function() {}); + $route_in_router = $this->router->getRoutes()[0]; + $this->assertSame($route, $route_in_router); + } + + public function testRouteSetAlias() { + $route = $this->router->map('/hi', function() {}); + $route->setAlias('hello'); + $this->assertEquals('hello', $route->alias); + } + // Test splat public function testSplatWildcard() { From 7fa71464d6faf12962b78d1a0fba9a0f6eefbf4a Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Sat, 13 Jan 2024 14:16:10 -0700 Subject: [PATCH 3/3] Added middleware documentation --- README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/README.md b/README.md index 86d4d48b..3e136d8f 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,65 @@ Flight::group('/users', function() { Flight::getUrl('user_view', [ 'id' => 5 ]); // will return '/users/5' ``` +## Route Middleware +Flight supports route and group route middleware. Middleware is a function that is executed before (or after) the route callback. This is a great way to add API authentication checks in your code, or to validate that the user has permission to access the route. + +Here's a basic example: + +```php +// If you only supply an anonymous function, it will be executed before the route callback. +// there are no "after" middleware functions except for classes (see below) +Flight::route('/path', function() { echo ' Here I am!'; })->addMiddleware(function() { + echo 'Middleware first!'; +}); + +Flight::start(); + +// This will output "Middleware first! Here I am!" +``` + +There are some very important notes about middleware that you should be aware of before you use them: +- Middleware functions are executed in the order they are added to the route. The execution is similar to how [Slim Framework handles this](https://www.slimframework.com/docs/v4/concepts/middleware.html#how-does-middleware-work). + - Befores are executed in the order added, and Afters are executed in reverse order. +- If your middleware function returns false, all execution is stopped and a 403 Forbidden error is thrown. You'll probably want to handle this more gracefully with a `Flight::redirect()` or something similar. +- If you need parameters from your route, they will be passed in a single array to your middleware function. (`function($params) { ... }` or `public function before($params) {}`). The reason for this is that you can structure your parameters into groups and in some of those groups, your parameters may actually show up in a different order which would break the middleware function by referring to the wrong parameter. This way, you can access them by name instead of position. + +### Middleware Classes + +Middleware can be registered as a class as well. If you need the "after" functionality, you must use a class. + +```php +class MyMiddleware { + public function before($params) { + echo 'Middleware first!'; + } + + public function after($params) { + echo 'Middleware last!'; + } +} + +$MyMiddleware = new MyMiddleware(); +Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware($MyMiddleware); // also ->addMiddleware([ $MyMiddleware, $MyMiddleware2 ]); + +Flight::start(); + +// This will display "Middleware first! Here I am! Middleware last!" +``` + +### Middleware Groups + +You can add a route group, and then every route in that group will have the same middleware as well. This is useful if you need to group a bunch of routes by say an Auth middleware to check the API key in the header. + +```php + +// added at the end of the group method +Flight::group('/api', function() { + Flight::route('/users', function() { echo 'users'; }, false, 'users'); + Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view'); +}, [ new ApiAuthMiddleware() ]); +``` + # Extending Flight is designed to be an extensible framework. The framework comes with a set