Merge pull request #514 from flightphp/middleware

Middleware code
pull/523/head v3.1.0
n0nag0n 1 year ago committed by GitHub
commit bef92303d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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 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 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, 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.
* @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,55 @@ 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 = $middleware_object($route->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 = $middleware_object($route->params);
if ($middleware_result === false) {
$failed_middleware_check = true;
break 2;
}
}
}
$dispatched = true;
@ -400,7 +447,9 @@ class Engine
$dispatched = false;
}
if (!$dispatched) {
if($failed_middleware_check === true) {
$this->halt(403, 'Forbidden');
} else if($dispatched === false) {
$this->notFound();
}
}
@ -464,21 +513,23 @@ 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);
}
/**
* 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<callable> $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);
}
/**

@ -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 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 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, 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.
* @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
*

@ -53,7 +53,8 @@ class Dispatcher
}
// 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 @@ class Dispatcher
{
$args = [&$params, &$output];
foreach ($filters as $callback) {
$continue = self::execute($callback, $args);
$continue = $callback(...$args);
if (false === $continue) {
break;
}
@ -178,27 +179,7 @@ class Dispatcher
*/
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 @@ class Dispatcher
$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();
}
/**

@ -136,30 +136,7 @@ class Loader
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);
}
/**

@ -57,6 +57,11 @@ final class Route
*/
public string $alias = '';
/**
* @var array<callable> 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>|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;
}
}

@ -26,7 +26,7 @@ class Router
public bool $case_sensitive = false;
/**
* Mapped routes.
* @var array<int, Route>
* @var array<int,Route>
*/
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<int,mixed> $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;
}
/**

@ -272,4 +272,165 @@ 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($params) { echo 'before'.$params['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($params) { echo 'before'.$params['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($params) {
echo 'before'.$params['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($params) {
echo 'before'.$params['id'];
}
public function after($params) {
echo 'after'.$params['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($params) {
echo 'after'.$params['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');
}
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');
}
}

@ -138,12 +138,6 @@ class LoaderTest extends PHPUnit\Framework\TestCase
$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() {

@ -298,6 +298,18 @@ class RouterTest extends PHPUnit\Framework\TestCase
$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()
{

Loading…
Cancel
Save