Skip to content

Commit

Permalink
Merge pull request #514 from flightphp/middleware
Browse files Browse the repository at this point in the history
Middleware code
  • Loading branch information
n0nag0n authored Jan 13, 2024
2 parents cb027f5 + 7fa7146 commit bef9230
Show file tree
Hide file tree
Showing 10 changed files with 381 additions and 123 deletions.
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 64 additions & 13 deletions flight/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

namespace flight;

use Closure;
use ErrorException;
use Exception;
use flight\core\Dispatcher;
Expand All @@ -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.
Expand All @@ -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 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
*
Expand Down Expand Up @@ -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);

Expand All @@ -383,11 +386,55 @@ 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 = $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;

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

if (!$dispatched) {
if($failed_middleware_check === true) {
$this->halt(403, 'Forbidden');
} else if($dispatched === false) {
$this->notFound();
}
}
Expand Down Expand Up @@ -464,21 +513,23 @@ 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);
}

/**
* 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);
}

/**
Expand Down
13 changes: 7 additions & 6 deletions flight/Flight.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 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
*
Expand Down
61 changes: 7 additions & 54 deletions flight/core/Dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'])) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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();
}

/**
Expand Down
25 changes: 1 addition & 24 deletions flight/core/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
30 changes: 30 additions & 0 deletions flight/net/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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>|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;
}
}
Loading

0 comments on commit bef9230

Please sign in to comment.