Middleware

Voir la vidéo
Description Sommaire

Même si vous n'avez jamais entendu le terme "middleware" le concept de ce design pattern ne devrait pas vous sembler étranger. Un middleware désigne un élément qui va venir se placer entre la requête reçue par le serveur et la réponse renvoyée. En ce sens, votre application PHP est, par nature, un middleware, elle prend en entrée une requête (représentée par des variables globales $_POST, $_SERVER...) et l'interprète pour renvoyer une réponse HTTP (grâce aux fonctions echo et header()).

Aussi il semble naturel de concevoir notre application de la même façon, sous forme de "tuyaux" qui reçoivent en entrée la requête, qui la traitent et la passe au tuyau suivant et qui font de même avec la réponse. C'est une philosophie proche du système de "pipe" utilisé sur Unix où l'on conçoit notre commande comme une combinaison d'opérations simples.nt

ps aux | grep apache | grep -v grep | awk '{print $2}' | xargs kill

Pourquoi maintenant ?

On peut en revanche se demander pourquoi ce système de middleware ne devient populaire que maintenant. L'engouement récent autour de ce pattern en PHP peut s'expliquer par l'arrivée de la recommandation PSR 7 qui définit, entre autre, une représentation sous forme d'objet de la requête et de la réponse.

Jusqu'à maintenant lorsque l'on souhaitait envoyer le corps d'une réponse on se contentait d'utiliser un echo et on utilisait header() pour renvoyer les en-têtes. Malheureusement, cette manière de faire les choses ne permet pas la combination(dès qu'un contenu est affiché il n'est plus possible de modifier les en têtes, de la même manière on ne peut pas modifier le contenu déjà afficher facilement).

Avec le PSR 7 on peut travailler avec un objet pour représenter la requête et la réponse. Objet que l'on peut transférer dans notre application.

$request = Request::fromGlobals(); // On génère le requête
$app = new App([
  'notFound' => function ($request) { return (new HTMLResponse('Not found'))->withStatus(404); }
]); // Ma super application
$response = $app->process($request); // On process la request, et on obtient une réponse
send($response); // On renvoie la réponse 

Middleware PSR 7

À partir des interfaces du PSR7 un modèle de conception a été créé pour représenter un middleware :

interface ServerMiddlewareInterface {
    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ): ResponseInterface;
}

Par exemple, si on souhaite rajouter un en-tête à chaque requête :

<?php
class PoweredByMiddleware implements ServerMiddlewareInterface
{

    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
    {
        $response = $next($request, $response);
        return $response->withHeader('X-Powered-By', 'Grafikart CMS over 9000');
    }
}

Ces middlewares sont représentés sous forme de simple callable (soit une closure, soit une classe avec une méthode __invoke). On peut ensuite les combiner naïvement :

$request = Request::fromGlobals();
$response = new Response();
$middleware1 = new PoweredByMiddleware();
$middleware2 = function ($request, $response, $next) {
  // ...
};
$response = $middleware1($request, $response, function ($request, $response) use ($middleware1) {
  return $middleware2($request, $response, function ($request, $response) {
    $response->getBody()->write('Salut les gens');
    return $response;
  });
});
send($response);

Ou utiliser un dispatcher pour combiner plus avec une syntaxe plus "naturelle" les middlewares.

$request = Request::fromGlobals();
$response = new Response();

// Nos middlewares
$middleware1 = new PoweredByMiddleware();
$middleware2 = function ($request, $response, $next) {
  // ...
};
// L'application est conçue comme un middleware
$app = function ($request, $response, $next) {
    $response->getBody()->write('Salut les gens');
    return $response;
}

// Le dispatcher permettant de "piper" request et response dans les middlewares
$dispatcher = new Dispatcher();
$dispatcher->pipe($middleware1);
$dispatcher->pipe($middleware2);
$dispatcher->pipe($app);
$response = $dispatcher->process($request, $response);

// On renvoie la réponse
send($response);

Middleware PSR 15

La signature vue précédemment a plusieurs problèmes qui ont été détaillés dans cet article (en) que je vous invite vivement à lire pour comprendre les changements apportés dans la PSR 15.

Mais pour résumer, le principal problème est que les middlewares peuvent pré-modifier la réponse avant de la passer au middleware suivant. Par exemple il est possible de réécrire notre premier middleware de la manière suivante.

<?php
class PoweredByMiddleware implements ServerMiddlewareInterface
{

    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
    {
        $response = $response->withHeader('X-Powered-By', 'Grafikart CMS over 9000');
        return $next($request, $response);
    }
}

Si un middleware situé plus bas dans la pile regénère une nouvelle réponse, alors toutes les modifications effectuées par les middlewares au dessus seront perdues.

$trailingSlash = function (ServerRequestInterface $request, ResponseInterface $response, callable $next) {
    $url = (string)$request->getUri();
    if (!empty($url) && $url[-1] === '/') {
        $response = new \GuzzleHttp\Psr7\Response();
        return $response
            ->withHeader('Location', substr($url, 0, -1))
            ->withStatus(301);
    }
    return $next($request, $response);
};

Ce middleware renvoie une nouvelle instance de la réponse et ignore la réponse qu'il a reçu en paramètre. Même s'il est placé après notre middleware PoweredBy il parasitera son comportement.

Aussi, une nouvelle interface est en cours de draft (PSR 15) et transforme la signature d'un middleware.

namespace Psr\Http\ServerMiddleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

interface MiddlewareInterface
{
    /**
     * Process an incoming server request and return a response, optionally delegating
     * to the next middleware component to create the response.
     *
     * @param ServerRequestInterface $request
     * @param DelegateInterface $delegate
     *
     * @return ResponseInterface
     */
    public function process(
        ServerRequestInterface $request,
        DelegateInterface $delegate
    ): ResponseInterface;
}

Cette interface permet de résoudre le problème précédent en retirant la réponse des paramètres et rajoute par la même occasion une interface pour représenter le système permettant d'appeler les middlewares suivants. Ainsi, avec cette nouvelle interface notre middleware PoweredBy ressemblerait à ça :

<?php
class PoweredByMiddleware implements MiddlewareInterface
{

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $response = $delegate->process($request);
        return $response->withHeader('X-Powered-By', 'Grafikart');
    }
}

Tout middleware !

L'approche middleware permet de décomposer notre application en briques réutilisables (sous conditions que les frameworks modernes adoptent le PSR 15 dès son acceptation). Par exemple, pour limiter l'accès à un backoffice il ne serait plus nécessaire de rajouter des conditions complexes mais simplement un middleware.

$dispatcher->pipe('/admin', new RoleRequiredMiddleware('admin'));

De la même manière le concept de controller peut être convertit sous forme de middleware.

<?php
class CrudMiddleware implements MiddlewareInterface {

  /**
   * Permet de déléguer le traitement de la requête vers des méthodes "controller friendly"
   * 
   * @param ServerRequestInterface $request
   * @param DelegateInterface $delegate
   * @return void
   */
  public function process(ServerRequestInterface $request, DelegateInterface $delegate) {
    $entity_id = $request->getAttribute($id);

    switch ($request->getMethod()) {
      case 'GET':
        switch ($entity_id) {
          case null:
            return $this->index($request);
          case 'new':
            return $this->create($request);
          default:
            return $this->edit($this->getEntity($id), $request);
        }
      case 'POST':
        return $this->store($request);
      case 'PUT':
        return $this->update($this->getEntity($post_id), $request);
      case 'DELETE':
        return $this->destroy($request);
    }
  }

  private function getEntity(int $id): Post 
  {
    return $this->getTable($this->table)->find($id);
  }

}

L'avantage ici est que l'on peut être plus modulable et si une action de notre application a un comportement très spécifique on pourra choisir de séparer la logique dans sa propre classe. On peut aussi s'imaginer combiner les middlewares pour séparer la logique de récupération dans la base de données.

$app->get('/post/{id}',[
  new PermissionRequired('post.show'), // Force une permission
  new EntityMidddleware('Post'), // Récupère l'entité et l'ajoute aux attributs de la requête
  new PostShowAction() // Le code qui va rendre notre vue
])
Publié
Technologies utilisées
Auteur :
Grafikart
Partager