diff --git a/flight/Engine.php b/flight/Engine.php index 44affc1..cacd19e 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace flight; +use Closure; use ErrorException; use Exception; use flight\core\Dispatcher; @@ -19,6 +20,7 @@ use flight\net\Response; 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 @@ use Throwable; * @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 @@ class Engine ob_start(); // Route the request + $failed_middleware_check = false; while ($route = $router->route($request)) { $params = array_values($route->params); @@ -383,11 +386,60 @@ class Engine $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 @@ class Engine $dispatched = false; } - if (!$dispatched) { + if($failed_middleware_check === true) { + $this->halt(403, 'Forbidden'); + } else if($dispatched === false) { $this->notFound(); } } @@ -464,10 +518,11 @@ class Engine * @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 fd3772c..c5b61e0 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -14,6 +14,7 @@ use flight\net\Request; 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 @@ use flight\template\View; * @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 63b1bf0..5f45392 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 @@ final class Route $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 f825616..cd49c9a 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 @@ class Router * @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 @@ class Router $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 @@ class Router * @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 @@ class Router * @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 @@ class Router * @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 @@ class Router * @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 @@ class Router * @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 @@ class Router * * @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 e1b9aaf..8c0697e 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -272,4 +272,77 @@ class EngineTest extends PHPUnit\Framework\TestCase $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'); + } }