Middleware

Middlewares provide a convenient mechanism to inspect and filter incoming HTTP requests to your application. For example, Lithe includes a middleware that checks if the user of your application is authenticated. If the user is not authenticated, the middleware will redirect the user to the login screen of your application. However, if the user is authenticated, the middleware will allow the request to proceed further into the application.

Functioning

In Lithe, Middleware are functions that have access to the request object ($req), response object ($res), and the $next function in the application's request-response cycle. The $next function is a function in Lithe's router that, when invoked, executes the next middleware in line after the current middleware.

Middleware functions provide a convenient mechanism to inspect, filter, and manipulate incoming HTTP requests to your application.

Middleware functions can perform the following tasks:

  • Execute any code.
  • Make changes to the request and response objects.
  • End the request-response cycle.
  • Call the next middleware in the stack.

If the current middleware function does not end the request-response cycle, it must call $next() to pass control to the next middleware function. Otherwise, the request will remain pending.


Elements of a Middleware Function

The following code shows the elements of a middleware function call:

$app->get('/', function ($req, $res, $next) {
    $next();
});

Where:

  • $req: HTTP request argument for the middleware function, conventionally called "$req".
  • $res: HTTP response argument for the middleware function, conventionally called "$res".
  • $next: Callback argument for the middleware function, conventionally called "$next".

Defining Middleware

Let's start with a simple example of middleware called myLogger. This middleware prints the message LOGGED every time a request passes through it. The middleware is defined as a function assigned to a variable called myLogger:

$myLogger = function ($req, $res, $next) {
  echo 'LOGGED';
  $next();
};

Note the call to $next() above. Calling this function invokes the next middleware function in the application. The $next() function is not part of PHP or Lithe but is the third argument passed to the middleware function. The $next() function could have any name, but by convention, it is always called "next". To avoid confusion, always use this convention.

Creating Middleware via Command Line

You can also create a new middleware using the make:middleware command:

php line make:middleware EnsureTokenIsValid

This command will place a new constant EnsureTokenIsValid within your application's http/middleware directory. In this middleware, we will only allow access to the route if the provided token matches a specified value. Otherwise, we will redirect users back to the initial URI:

use Lithe\Http\Request;
use Lithe\Http\Response;

define('EnsureTokenIsValid', function (Request $req, Response $res, $next) {
    $token = $req->input('token');

    if ($token !== 'my-secret-token') {
        return $res->redirect('home');
    }

    return $next();
});

As you can see, if the provided token does not match our secret token, the middleware will return an HTTP redirect to the client; otherwise, the request will proceed further into the application. To pass the request deeper into the application (allowing the middleware to "pass"), you must call the callback $next.

It is best to think of middleware as a series of "layers" that HTTP requests must traverse before reaching your application. Each layer can inspect the request and even reject it completely.


Loading Middleware

To load a middleware function, you can call the use() method of the \Lithe\App class or use its shortcut, the \Lithe\Orbs\Http\Router\apply function, specifying the desired middleware function. For example, the following code loads the myLogger middleware function before executing the route for the root path (/):

$app = \Lithe\App::mount();

$myLogger = function ($req, $res, $next) {
    echo 'LOGGED';
    $next();
};

$app->use($myLogger);

$app->get('/', function ($req, $res, $next) {
    $res->send('Hello World!');
});

Whenever the application receives a request, it prints the message "LOGGED".

The order of loading middleware is important: middleware functions that are loaded first are also executed first.

The myLogger middleware function simply prints a message and then passes the request to the next middleware function in the stack by calling the $next() function.


Using Middleware

A Lithe application can use the following types of middleware:

Application-level Middleware

Bind application-level middleware to an instance of the application object using the use() and METHOD() methods, where METHOD is the HTTP method of the request that the middleware function handles (such as GET, PUT, or POST) in lowercase.

The following example shows a middleware function without a mount path. The function is executed every time the application receives a request.

$app = \Lithe\App::mount();

$app->use(function ($req, $res, $next) {
    echo 'Hello World!';
    $next();
});

The example below shows a middleware that handles GET requests at the path /user/:id.

$app->get('/user/:id', function ($req, $res, $next) {
    // If the user ID is '0', pass to the next middleware
    if ($req->params->id === '0') {
        return $next();
    } 

    // Otherwise, send a specific response
    $res->send('ID is not 0');
}, function ($req, $res) {
    // Send a response when the ID is '0'
    $res->send('regular');
});

Router-level Middleware

Router-level middleware works the same way as application-level middleware, except it is bound to an instance of \Lithe\Http\Router.

$router = new \Lithe\Http\Router;

Load router-level middleware using the use and METHOD functions.

The following example code shows a middleware system using router-level middleware:

$router = new \Lithe\Http\Router();

// Router-level middleware for all routes in the router
$router->use(function ($req, $res, $next) {
    echo 'Time: ', Date('H:i:s'), '<br>';
    $next();
});

$router->get('/user/:id', function ($req, $res, $next) {
    // Check if the user ID is '0'; if so, redirect to /
    if ($req->params->id === '0') {
        $res->redirect('/');
    }
    // Otherwise, pass control to the next middleware in the stack
    $next();
}, function ($req, $res, $next) {
    echo $req->params->id;
    $res->render('special');
});

// Mount the router to the application
$app->use('/api', $router);

Third-party Middleware

Use third-party middleware to add functionalities to Lithe applications.

Install the necessary PHP module for the desired functionality and then load it into your application at the application or router level.

The following example illustrates loading session middleware, the function \Lithe\Middleware\Session\session.

use function Lithe\Middleware\Session\session;

$app = \Lithe\App::mount();

$app->use(session([
    'secure' => true
]));

For a partial list of commonly used third-party middleware functions with Lithe, see: Third-party Middleware.

Configurable Middleware

If you need your middleware to be configurable, export a function that accepts an array of options or other parameters, and then return the middleware implementation based on the input parameters.

my-middleware.php
<?php
return function ($options) {
    return function ($req, $res, $next) use ($options) {
        // Middleware implementation based on the options array
        
        // Call the next middleware
        $next();
    };
};

Now, the middleware can be used as shown below.

$mw = require(__DIR__ .'/my-middleware.php');

$app->use($mw(['option1' => '1', 'option2' => '2']));