Skip to content

Commit bef9230

Browse files
authored
Merge pull request #514 from flightphp/middleware
Middleware code
2 parents cb027f5 + 7fa7146 commit bef9230

File tree

10 files changed

+381
-123
lines changed

10 files changed

+381
-123
lines changed

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,65 @@ Flight::group('/users', function() {
409409
Flight::getUrl('user_view', [ 'id' => 5 ]); // will return '/users/5'
410410
```
411411

412+
## Route Middleware
413+
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.
414+
415+
Here's a basic example:
416+
417+
```php
418+
// If you only supply an anonymous function, it will be executed before the route callback.
419+
// there are no "after" middleware functions except for classes (see below)
420+
Flight::route('/path', function() { echo ' Here I am!'; })->addMiddleware(function() {
421+
echo 'Middleware first!';
422+
});
423+
424+
Flight::start();
425+
426+
// This will output "Middleware first! Here I am!"
427+
```
428+
429+
There are some very important notes about middleware that you should be aware of before you use them:
430+
- 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).
431+
- Befores are executed in the order added, and Afters are executed in reverse order.
432+
- 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.
433+
- 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.
434+
435+
### Middleware Classes
436+
437+
Middleware can be registered as a class as well. If you need the "after" functionality, you must use a class.
438+
439+
```php
440+
class MyMiddleware {
441+
public function before($params) {
442+
echo 'Middleware first!';
443+
}
444+
445+
public function after($params) {
446+
echo 'Middleware last!';
447+
}
448+
}
449+
450+
$MyMiddleware = new MyMiddleware();
451+
Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware($MyMiddleware); // also ->addMiddleware([ $MyMiddleware, $MyMiddleware2 ]);
452+
453+
Flight::start();
454+
455+
// This will display "Middleware first! Here I am! Middleware last!"
456+
```
457+
458+
### Middleware Groups
459+
460+
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.
461+
462+
```php
463+
464+
// added at the end of the group method
465+
Flight::group('/api', function() {
466+
Flight::route('/users', function() { echo 'users'; }, false, 'users');
467+
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
468+
}, [ new ApiAuthMiddleware() ]);
469+
```
470+
412471
# Extending
413472

414473
Flight is designed to be an extensible framework. The framework comes with a set

flight/Engine.php

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
namespace flight;
1212

13+
use Closure;
1314
use ErrorException;
1415
use Exception;
1516
use flight\core\Dispatcher;
@@ -19,6 +20,7 @@
1920
use flight\net\Router;
2021
use flight\template\View;
2122
use Throwable;
23+
use flight\net\Route;
2224

2325
/**
2426
* The Engine class contains the core functionality of the framework.
@@ -32,12 +34,12 @@
3234
* @method void halt(int $code = 200, string $message = '') Stops processing and returns a given response.
3335
*
3436
* Routing
35-
* @method void route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a URL to a callback function with all applicable methods
36-
* @method void group(string $pattern, callable $callback) Groups a set of routes together under a common prefix.
37-
* @method void post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a POST URL to a callback function.
38-
* @method void put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PUT URL to a callback function.
39-
* @method void patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PATCH URL to a callback function.
40-
* @method void delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a DELETE URL to a callback function.
37+
* @method Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a URL to a callback function with all applicable methods
38+
* @method void group(string $pattern, callable $callback, array $group_middlewares = []) Groups a set of routes together under a common prefix.
39+
* @method Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a POST URL to a callback function.
40+
* @method Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PUT URL to a callback function.
41+
* @method Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PATCH URL to a callback function.
42+
* @method Route delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a DELETE URL to a callback function.
4143
* @method Router router() Gets router
4244
* @method string getUrl(string $alias) Gets a url from an alias
4345
*
@@ -375,6 +377,7 @@ public function _start(): void
375377
ob_start();
376378

377379
// Route the request
380+
$failed_middleware_check = false;
378381
while ($route = $router->route($request)) {
379382
$params = array_values($route->params);
380383

@@ -383,11 +386,55 @@ public function _start(): void
383386
$params[] = $route;
384387
}
385388

389+
// Run any before middlewares
390+
if(count($route->middleware) > 0) {
391+
foreach($route->middleware as $middleware) {
392+
393+
$middleware_object = (is_callable($middleware) === true ? $middleware : (method_exists($middleware, 'before') === true ? [ $middleware, 'before' ]: false));
394+
395+
if($middleware_object === false) {
396+
continue;
397+
}
398+
399+
// It's assumed if you don't declare before, that it will be assumed as the before method
400+
$middleware_result = $middleware_object($route->params);
401+
402+
if ($middleware_result === false) {
403+
$failed_middleware_check = true;
404+
break 2;
405+
}
406+
}
407+
}
408+
386409
// Call route handler
387410
$continue = $this->dispatcher->execute(
388411
$route->callback,
389412
$params
390413
);
414+
415+
416+
// Run any before middlewares
417+
if(count($route->middleware) > 0) {
418+
419+
// process the middleware in reverse order now
420+
foreach(array_reverse($route->middleware) as $middleware) {
421+
422+
// must be an object. No functions allowed here
423+
$middleware_object = is_object($middleware) === true && !($middleware instanceof Closure) && method_exists($middleware, 'after') === true ? [ $middleware, 'after' ] : false;
424+
425+
// has to have the after method, otherwise just skip it
426+
if($middleware_object === false) {
427+
continue;
428+
}
429+
430+
$middleware_result = $middleware_object($route->params);
431+
432+
if ($middleware_result === false) {
433+
$failed_middleware_check = true;
434+
break 2;
435+
}
436+
}
437+
}
391438

392439
$dispatched = true;
393440

@@ -400,7 +447,9 @@ public function _start(): void
400447
$dispatched = false;
401448
}
402449

403-
if (!$dispatched) {
450+
if($failed_middleware_check === true) {
451+
$this->halt(403, 'Forbidden');
452+
} else if($dispatched === false) {
404453
$this->notFound();
405454
}
406455
}
@@ -464,21 +513,23 @@ public function _stop(?int $code = null): void
464513
* @param callable $callback Callback function
465514
* @param bool $pass_route Pass the matching route object to the callback
466515
* @param string $alias the alias for the route
516+
* @return Route
467517
*/
468-
public function _route(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): void
518+
public function _route(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route
469519
{
470-
$this->router()->map($pattern, $callback, $pass_route, $alias);
520+
return $this->router()->map($pattern, $callback, $pass_route, $alias);
471521
}
472522

473523
/**
474524
* Routes a URL to a callback function.
475525
*
476-
* @param string $pattern URL pattern to match
477-
* @param callable $callback Callback function that includes the Router class as first parameter
526+
* @param string $pattern URL pattern to match
527+
* @param callable $callback Callback function that includes the Router class as first parameter
528+
* @param array<callable> $group_middlewares The middleware to be applied to the route
478529
*/
479-
public function _group(string $pattern, callable $callback): void
530+
public function _group(string $pattern, callable $callback, array $group_middlewares = []): void
480531
{
481-
$this->router()->group($pattern, $callback);
532+
$this->router()->group($pattern, $callback, $group_middlewares);
482533
}
483534

484535
/**

flight/Flight.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use flight\net\Response;
1515
use flight\net\Router;
1616
use flight\template\View;
17+
use flight\net\Route;
1718

1819
/**
1920
* The Flight class is a static representation of the framework.
@@ -23,12 +24,12 @@
2324
* @method static void stop() Stops the framework and sends a response.
2425
* @method static void halt(int $code = 200, string $message = '') Stop the framework with an optional status code and message.
2526
*
26-
* @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.
27-
* @method static void group(string $pattern, callable $callback) Groups a set of routes together under a common prefix.
28-
* @method static void post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a POST URL to a callback function.
29-
* @method static void put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PUT URL to a callback function.
30-
* @method static void patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PATCH URL to a callback function.
31-
* @method static void delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a DELETE URL to a callback function.
27+
* @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.
28+
* @method static void group(string $pattern, callable $callback, array $group_middlewares = []) Groups a set of routes together under a common prefix.
29+
* @method static Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a POST URL to a callback function.
30+
* @method static Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PUT URL to a callback function.
31+
* @method static Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a PATCH URL to a callback function.
32+
* @method static Route delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') Routes a DELETE URL to a callback function.
3233
* @method static Router router() Returns Router instance.
3334
* @method static string getUrl(string $alias) Gets a url from an alias
3435
*

flight/core/Dispatcher.php

Lines changed: 7 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ final public function run(string $name, array $params = [])
5353
}
5454

5555
// Run requested method
56-
$output = self::execute($this->get($name), $params);
56+
$callback = $this->get($name);
57+
$output = $callback(...$params);
5758

5859
// Run post-filters
5960
if (!empty($this->filters[$name]['after'])) {
@@ -140,7 +141,7 @@ final public function filter(array $filters, array &$params, &$output): void
140141
{
141142
$args = [&$params, &$output];
142143
foreach ($filters as $callback) {
143-
$continue = self::execute($callback, $args);
144+
$continue = $callback(...$args);
144145
if (false === $continue) {
145146
break;
146147
}
@@ -178,27 +179,7 @@ public static function execute($callback, array &$params = [])
178179
*/
179180
public static function callFunction($func, array &$params = [])
180181
{
181-
// Call static method
182-
if (\is_string($func) && false !== strpos($func, '::')) {
183-
return \call_user_func_array($func, $params);
184-
}
185-
186-
switch (\count($params)) {
187-
case 0:
188-
return $func();
189-
case 1:
190-
return $func($params[0]);
191-
case 2:
192-
return $func($params[0], $params[1]);
193-
case 3:
194-
return $func($params[0], $params[1], $params[2]);
195-
case 4:
196-
return $func($params[0], $params[1], $params[2], $params[3]);
197-
case 5:
198-
return $func($params[0], $params[1], $params[2], $params[3], $params[4]);
199-
default:
200-
return \call_user_func_array($func, $params);
201-
}
182+
return call_user_func_array($func, $params);
202183
}
203184

204185
/**
@@ -215,37 +196,9 @@ public static function invokeMethod($func, array &$params = [])
215196

216197
$instance = \is_object($class);
217198

218-
switch (\count($params)) {
219-
case 0:
220-
return ($instance) ?
221-
$class->$method() :
222-
$class::$method();
223-
case 1:
224-
return ($instance) ?
225-
$class->$method($params[0]) :
226-
$class::$method($params[0]);
227-
case 2:
228-
return ($instance) ?
229-
$class->$method($params[0], $params[1]) :
230-
$class::$method($params[0], $params[1]);
231-
case 3:
232-
return ($instance) ?
233-
$class->$method($params[0], $params[1], $params[2]) :
234-
$class::$method($params[0], $params[1], $params[2]);
235-
// This will be refactored soon enough
236-
// @codeCoverageIgnoreStart
237-
case 4:
238-
return ($instance) ?
239-
$class->$method($params[0], $params[1], $params[2], $params[3]) :
240-
$class::$method($params[0], $params[1], $params[2], $params[3]);
241-
case 5:
242-
return ($instance) ?
243-
$class->$method($params[0], $params[1], $params[2], $params[3], $params[4]) :
244-
$class::$method($params[0], $params[1], $params[2], $params[3], $params[4]);
245-
default:
246-
return \call_user_func_array($func, $params);
247-
// @codeCoverageIgnoreEnd
248-
}
199+
return ($instance) ?
200+
$class->$method(...$params) :
201+
$class::$method();
249202
}
250203

251204
/**

flight/core/Loader.php

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -136,30 +136,7 @@ public function newInstance($class, array $params = [])
136136
return \call_user_func_array($class, $params);
137137
}
138138

139-
switch (\count($params)) {
140-
case 0:
141-
return new $class();
142-
case 1:
143-
return new $class($params[0]);
144-
// @codeCoverageIgnoreStart
145-
case 2:
146-
return new $class($params[0], $params[1]);
147-
case 3:
148-
return new $class($params[0], $params[1], $params[2]);
149-
case 4:
150-
return new $class($params[0], $params[1], $params[2], $params[3]);
151-
case 5:
152-
return new $class($params[0], $params[1], $params[2], $params[3], $params[4]);
153-
// @codeCoverageIgnoreEnd
154-
default:
155-
try {
156-
$refClass = new ReflectionClass($class);
157-
158-
return $refClass->newInstanceArgs($params);
159-
} catch (ReflectionException $e) {
160-
throw new Exception("Cannot instantiate {$class}", 0, $e);
161-
}
162-
}
139+
return new $class(...$params);
163140
}
164141

165142
/**

flight/net/Route.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ final class Route
5757
*/
5858
public string $alias = '';
5959

60+
/**
61+
* @var array<callable> The middleware to be applied to the route
62+
*/
63+
public array $middleware = [];
64+
6065
/**
6166
* Constructor.
6267
*
@@ -190,4 +195,29 @@ public function hydrateUrl(array $params = []): string {
190195
$url = rtrim($url, '/');
191196
return $url;
192197
}
198+
199+
/**
200+
* Sets the route alias
201+
*
202+
* @return self
203+
*/
204+
public function setAlias(string $alias): self {
205+
$this->alias = $alias;
206+
return $this;
207+
}
208+
209+
/**
210+
* Sets the route middleware
211+
*
212+
* @param array<callable>|callable $middleware
213+
* @return self
214+
*/
215+
public function addMiddleware($middleware): self {
216+
if(is_array($middleware) === true) {
217+
$this->middleware = array_merge($this->middleware, $middleware);
218+
} else {
219+
$this->middleware[] = $middleware;
220+
}
221+
return $this;
222+
}
193223
}

0 commit comments

Comments
 (0)