Close search
Hoa

Manuel d'apprentissage

Router + Dispatcher = ♥

Un aspect aussi nécessaire qu'important est l'assignation des tâches en fonction des requêtes effectuées sur l'application. Dans ce chapitre, nous allons présenter deux bibliothèques de Hoa : Hoa\Router et Hoa\Dispatcher afin de construire notre première application.

Table des matières

  1. Introduction
  2. Un peu d'histoire
  3. Aperçu
  4. Routeur HTTP
    1. Écrire une règle
    2. Bien comprendre les variables et les motifs
    3. Router et dérouter
  5. Dispatcheur
    1. Pas si basic que ça
    2. Notion de kit
    3. Vers un MVC
  6. Application : Gordon's blog

Introduction

Maintenant que nous connaissons un peu le fonctionnement de Hoa et que nous avons fait connaissance avec quelques aspects framework, nous allons entamer notre première application.

Nous allons tout d'abord commencer par un peu d'histoire pour bien comprendre comment fonctionne une application Web. Par la suite, nous donnerons un aperçu du fonctionnement, qui se résumera à écrire et tester quelques règles. Puis nous expliquerons le fonctionnement du routeur et du dispatcheur utilisés dans l'aperçu. Et enfin, nous commencerons un blog.

Un peu d'histoire

Contrairement à des applications de bureaux, les applications Web ne restent pas en exécution durant toute leur utilisation (même s'il existe quelques exceptions, nous préfèrons rester dans les généralités). PHP ne déroge pas à la règle. En effet, quand nous effectuons une demande auprès de l'application (par exemple : « je veux telle page »), PHP reçoit la demande, exécute la tâche associée à la demande, fournit une réponse et s'arrête. Ceci est le fonctionnement schématique type de PHP.

Nous ne parlons pas réellement de demande mais plutôt de requête. La majorité des requêtes sur le Web sont exprimées sous le format HTTP (pour HyperText Transfer Protocol). Seulement trois versions d'HTTP ont été écrites : RFC1945 pour HTTP1.0, RFC2068 pour HTTP1.1 et RFC2616 pour la version finale d'HTTP1.1, que nous utilisons actuellement partout. La personne qui émet une requête est un client (souvent un navigateur Web). Cette requête est envoyée à un serveur, qui connait votre application. Si nous entrons un peu plus dans le détail, côté serveur, un programme filtre la réception des requêtes. Dans le cas d'HTTP, nous appellons ce programme un serveur HTTP. Quand ce serveur HTTP reçoit une requête, il regarde quel programme est susceptible d'être concerné par cette requête et lui redirige la requête en attendant sa réponse, qu'il renverra alors au client.

Un protocole standard a été défini pour faciliter la communication entre le serveur HTTP et le programme qui va réceptionner la requête. Nous appellons ce protocole : CGI (pour Common Gateway Interface), définie par la RFC3875. Toutefois, ce protocole n'était plus suffisant pour répondre aux nombres grandissants de requêtes sur les serveurs. C'est pourquoi, FastCGI (pour Fast Common Gateway Interface) a été créé.

PHP fonctionne derrière FastCGI pour recevoir les communications du serveur HTTP. La requête HTTP reçue fournit toutes les informations nécessaires pour que PHP exécute une tâche sans problème.

Une requête HTTP est principalement caractérisée par une méthode. Nous en dénombrons neuf au total : CONNECT, DELETE, GET, HEAD, OPTIONS, POST, PUT, PATCH et TRACE. Nous sommes capable bien sûr d'ajouter nos propres méthodes mais l'objectif ici n'est pas de faire un cours sur HTTP. À chaque méthode est associée une URI (pour Uniform Resource Identifier), standardisée par la RFC3986. Ainsi, l'exemple suivant est une requête qui exprime « je souhaite obtenir la ressource /path/to/resource » :

GET /path/to/resource

Aperçu

Un aperçu est ici utile pour faire quelques essais avant de détailler le fonctionnement. Nous écrivons l'exemple suivant dans le fichier Application/Public/index.php :

$router = new Hoa\Router\Http();
$router
    ->get('u', '/hello', function ( ) {

        echo 'world!', "\n";
    })
    ->get('v', '/bye', function ( ) {

        echo 'ohh :-(', "\n";
    })
    ->post('w', '/hello', function ( Array $_request ) {

        echo $_request['a'] + $_request['b'], "\n";
    })
    ->get('x', '/hello_(?<nick>\w+)', function ( $nick ) {

        echo 'Welcome ', ucfirst($nick), '!', "\n";
    });

$dispatcher = new Hoa\Dispatcher\Basic();
$dispatcher->dispatch($router);

Et pour tester notre routeur et notre dispatcheur, nous allons nous appuyer sur Bhoa (un serveur HTTP fourni avec Hoa, réservé à du développement seulement) et cURL (pour Client URL Request Library), un client en ligne de commande permettant de manipuler plusieurs protocoles, dont HTTP. Tout d'abord, démarrons PHP FastCGI et Bhoa :

$ php-cgi -b 127.0.0.1:9000&
$ myapp http:bhoa --root hoa://Application/Public

Puis dans un autre terminal, envoyons-lui une requête avec cURL :

$ curl 127.0.0.1:8888/hello
world!
$ curl 127.0.0.1:8888/bye
ohh :-(
$ curl -X POST -d a=3\&b=39 127.0.0.1:8888/hello
42
$ curl -X POST 127.0.0.1:8888/bye
// error
$ curl 127.0.0.1:8888/hello_gordon
Welcome Gordon!
$ curl 127.0.0.1:8888/hello_alyx
Welcome Alyx!

Par défaut, cURL va envoyer une requête HTTP avec la méthode GET. Nous remarquons que si nous appellons /hello en GET ou en POST, cela n'a pas le même effet. De même, /bye n'est accessible qu'avec la méthode GET. Enfin, nous remarquons comment nous pouvons avoir des règles dynamiques et comment nous pouvons capturer certaines parties que nous retrouvons en argument de nos actions.

Dans cet aperçu, nous avons utilisé un routeur et un dispatcheur. Nous allons, dans un premier temps, détailler le fonctionnement du routeur, puis, dans un second temps, celui du dispatcheur.

Routeur HTTP

Nous allons nous concentrer sur les explications relatives au routeur HTTP, à savoir la classe Hoa\Router\Http.

Écrire une règle

Une règle est définie par un t-uplet :

La liste des méthodes va permettre de filtrer les règles en fonction de la méthode de la requête HTTP reçue. Ainsi, si nous recevons une requête avec une méthode GET, nous n'allons sélectionner que les règles qui acceptent cette méthode. La liste exprime la capacité d'une règle à accepter plusieurs méthodes.

Le motif s'exprime sous forme d'une expression régulière qui s'appliquera sur le chemin de l'URI (voir la section 3, Syntax Components, de la RFC3986). Note : nous sommes également capable d'étendre le motif aux sous-domaines.

Pour écrire une règle, nous utilisons la méthode Hoa\Router\Http::addRule qui ajoute une règle avec une visibilité publique, i.e. nous ne nous occuperons pas de la visibilité, elle sera gérée toute seule. Ainsi :

$router = new Hoa\Router\Http();
$router->addRule($id, $methods, $pattern, $call, $able, $variables);

Le dernier argument $variables est évidemment optionnel puisqu'il sert à déclarer de nouvelles variables ou des valeurs par défaut pour les variables. Parfois, les arguments $call et $able peuvent être partiellement ou totalement optionels selon le contexte.

Si nous devions exprimer la règle u de notre aperçu, on aurait ceci :

$router->addRule('u', array('get'), '/hello', function ( ) { … });

Par soucis de simplicité, nous pouvons ne pas utiliser la méthode Hoa\Router\Http::addRule mais invoquer un peu de magie ! En effet, l'écriture qui va suivre est strictement identique à la précédente :

$router->get('u', '/hello', function ( ) { … });

Voici comment opère la magie : les appels vers une méthode method[_method]*(…) seront redirigés vers la méthode addRule(…, array('method'[, 'method']*), …). Voyons plutôt des exemples d'équivalences :

$router->get(…);
$router->addRule(…, array('get'), …);

$router->get_post(…);
$router->addRule(…, array('get', 'post'), …);

$router->put_delete_get(…);
$router->addRule(…, array('put', 'delete', 'get'), …);

// etc.

L'ordre des règle a une importance, car les règles sont filtrées puis évaluées dans l'ordre de déclaration. Il est alors conseillé décrire les règles spécifiques avant les règles génériques.

Nous conviendrons de la simplicité qu'introduit cette magie.

Bien comprendre les variables et les motifs

Le rôle principal d'un routeur est d'extraire des données d'une requête à partir d'une règle afin de les placer dans des variables (nous noterons que les routeurs savent faire l'opération inverse). C'est pourquoi nous sommes capable de définir des valeurs par défaut pour certaines variables si elles ne sont pas définies ou alors de définir de nouvelles variables. Ils existent des variables qui peuvent être définies par le routeur lui-même (toujours préfixées par le symbole « _ »). Nous éviterons l'utilisation des noms de variables utilisant ce préfixe.

Les expressions régulières dans PHP sont basées sur les PCRE qui ont un pouvoir d'expression plus avancé que de simples expressions régulières traditionnelles. Elles permettent entre autre de nommer des motifs, en plus de les indexer automatiquement, grâce à la syntaxe (?<name>pattern) (nous retrouverons la syntaxe des PCRE à la section pcresyntax(3)). Chaque motif capturé et nommé se retrouvera dans une variable, au niveau du routeur, du même nom que le motif. C'est pourquoi, lorsque nous écrivons /hello_(?<nick>\w+), une variable nick sera créée dans le routeur et aura pour valeur la donnée capturée par le motif \w+.

Toutes les variables définies par le routeur lors de l'interprétation d'une règle seront prioritaires sur les variables attachées à la règle par l'utilisateur. Ainsi nous retrouvons un comportement de valeur par défaut.

Router et dérouter

Maintenant que nous avons déclaré plusieurs règles auprès du routeur, nous allons lui demander de déterminer la règle qui correspond à notre requête. Chaque routeur a une méthode route où tous ses arguments doivent avoir une valeur par défaut. Dans le cas du routeur HTTP, les arguments sont l'URI et le préfixe (comprendre, la base) de notre URI. Ses valeurs par défaut sont automatiquement déduites à partir des données provenant du serveur. Une fois le routage terminé, nous utilisons la méthode getTheRule pour obtenir la règle qui a servi à extraire nos données de notre requête. Nous précisons que les constantes Hoa\Router\Router::RULE_* existent pour manipuler le t-uplet représentant une règle. Ainsi, à partir de notre exemple :

$router->route('/root/hello_gordon', '/root');
print_r($router->getTheRule());

/**
 * Will output:
 *      Array
 *      (
 *          [0] => 0
 *          [1] => Array
 *              (
 *                  [0] => get
 *              )
 *
 *          [2] => x
 *          [3] => /hello_(?<nick>\w+)
 *          [4] => Closure Object…
 *
 *          [5] =>
 *          [6] => Array
 *              (
 *                  [_domain] =>
 *                  [_subdomain] =>
 *                  [_call] => Closure Object…
 *                  [_able] =>
 *                  [_request] => Array
 *                      (
 *                      )
 *
 *                  [nick] => gordon
 *              )
 *
 *      )
 */

Pour éviter de préciser le préfixe du chemin à chaque fois, nous pouvons utiliser la méthode setPrefix dont le seul argument est le préfixe à considérer.

À l'instar de la méthode route, chaque routeur définit la méthode unroute qui permet d'écrire une requête correspond à une règle. Pour cela, nous devons préciser deux arguments à cette méthode : le premier étant l'identifiant de la règle et le second un tableau de variables. Chaque nom de variable correspond au nom d'un motif. Par exemple :

var_dump($router->unroute('x', array('nick' => 'gordon')));

/**
 * Will output:
 *     string(13) "/hello_gordon"
 */

Le mécanisme de routage peut être effectué automatiquement par les dispatcheurs, comme nous allons le voir.

Dispatcheur

Un dispatcheur est souvent couplé à un routeur mais pas nécessairement. Selon la terminologie des dispatcheurs, son rôle est d'être capable de dispatcher une tâche définie par certaines données sur un contrôleur et une action en fonction de la nature des données manipulées. Si nous utilisons la terminologie de Hoa, un couple contrôleur et action est strictement équivalent à un couple call et able, seule la terminologie diffère, le comportement restant le même et étant connu.

Les dispatcheurs sont définis par une classe abstraite Hoa\Dispatcher. Cette classe propose d'utiliser la règle sélectionnée par un routeur pour savoir vers quels contrôleurs et actions rediriger les données extraites du routeur. Notre point d'entrée sera la méthode dispatch sur tout dispatcheur. Nous allons particulièrement nous intéresser au dispatcheur proposé par défaut, à savoir Hoa\Dispatcher\Basic.

Pas si basic que ça

La classe Hoa\Dispatcher\Basic supporte trois types de contrôleurs et d'actions : fonctions anonymes en tant que contrôleurs (pas d'action), fonctions déclarées en tant que contrôleurs (pas d'action) et classes et méthodes en tant que contrôleurs et actions. Dans le dernier cas, des règles de nommage sont proposées : Application\Controller\ControllerName pour le contrôleur (qui est alors une classe) et ActionNameAction pour l'action (qui est alors une méthode). Nous pouvons modifier ces règles de nommage facilement. De même, il est proposé des règles de nommage différentes pour des appels synchrones ou asynchrones ; nous proposons ActionNameActionAsync pour les actions asynchrones. Un appel asynchrone peut s'effectuer par exemple à travers XHR (pour XML HTTP Request), protocole standardisée par le W3C.

Le routeur HTTP définit les variables _call et _able qui correspondent aux composantes call et able lors d'une définition de règle de routeur. Ces variables vont être utilisées pour le nom du contrôleur et de l'action par le dispatcheur. Dans un souci de cohérence dans la terminologie, nous pouvons utiliser les variables controller et action dans le routeur, ce qui sera compris de la même manière par le dispatcheur basic et qui aura le mérite de ne pas réécrire _call et _able.

Dans l'aperçu, nous avons utilisé des fonctions anonymes. Nous allons à présent utiliser une partie de cet aperçu mais en le transformant pour avoir des fonctions déclarées :

$router = new Hoa\Router\Http();
$router->get('u', '/hello', 'hello')
       ->get('v', '/bye', 'bye');

function hello ( ) {

    echo 'world!', "\n";
}

function bye ( ) {

    echo 'ohh :-(', "\n";
}

$dispatcher = new Hoa\Dispatcher\Basic();
$dispatcher->dispatch($router);

Nous remarquons que pour /hello, nous appellons la fonction hello. Pareil pour /bye et bye. Le nom du contrôleur est identique à la partie à droite du symbole « / » dans la règle. Nous pouvons alors simplifier les règles en capturant la valeur de la variable controller de cette manière :

$router = new Hoa\Router\Http();
$router->get('u', '/(?<controller>hello|bye)');

Nous pouvons imaginer utiliser le motif \w+ à la place de hello|bye pour avoir plus de dynamisme (mais il ne faudra pas oublier de gérer les erreurs si l'action n'existe pas, grâce à l'exception Hoa\Router\Exception\NotFound).

En plus de gérer trois types de contrôleurs et d'actions, le dispatcheur basic sait distribuer les variables sur les arguments des actions. C'est pourquoi quand nous écrivons la règle /hello_(?<nick>\w+), nous pouvons retrouver la valeur de la variable nick directement dans notre action dans son argument $nick ; de cette manière :

$router->get('x', 'hello_(?<nick>\w+)', function ( $nick ) {

    echo 'Welcome ', ucfirst($nick), '!', "\n";
});

L'ordre des arguments n'a aucune importance, seul le nom est important. Nous pouvons retrouver aussi bien les variables extraites des règles que celles définies par le routeur (i.e. préfixées par le symbole « _ »). C'est ce que nous faisons avec /hello en POST et l'argument $_request qui est une variable définie par le routeur et qui contient des données d'une requête HTTP non-accessible via l'URI.

Notion de kit

Basiquement, un kit joue le rôle de lien entre les bibliothèques et l'application en rassemblant, proposant ou exposant certaines fonctionnalités. Par exemple, quand le dispatcheur appelle un contrôleur et une action, il peut transmettre des données provenant du routeur. Toutefois, nous aimerions en profiter pour transmettre plus d'informations en même temps. C'est à ce moment qu'un kit peut être intéressant.

Un kit contient de base le routeur, le dispatcheur, une vue (simplement n'importe quel objet de type Hoa\View\Viewable) et des données (de type Hoa\Core\Data). La création d'un kit se fait automatiquement, tout ce qui est nécessaire est de fournir le nom du kit au dispatcheur grâce à la méthode Hoa\Dispatcher\Dispatcher::setKitName. Par exemple, nous créons un kit qui va ajouter une méthode listRequestKeys se basant sur le routeur HTTP :

class MyKit extends Hoa\Dispatcher\Kit {

    public function listRequestKeys ( ) {

        $theRule = $this->router->getTheRule();

        return array_keys($theRule[Hoa\Router\Router::RULE_VARIABLES]['_request']);
    }
}

$dispatcher = new Hoa\Dispatcher\Basic();
$dispatcher->setKitName('MyKit');

Et maintenant, toutes les actions ayant ce kit pourront appeler la méthode listRequestKeys.

Le dispatcheur basic Hoa\Dispatcher\Basic ajoute le kit en tant que variable du routeur sous le nom _this. Ainsi, si on veut récupérer le kit depuis une action, nous n'aurons qu'à avoir un argument $_this :

$router = new Hoa\Router\Http();
$router->get_post('u', '/(?<controller>hello)');

function hello ( MyKit $_this ) {

    print_r($_this->listRequestKeys());
}

$dispatcher->dispatch($router);

Nous testons notre nouveau kit :

$ curl -X POST -d foo=bar\&baz=qux 127.0.0.1:8888/hello
Array
(
    [0] => foo
    [1] => baz
)

Le dispatcheur basic va plus loin quand le contrôleur est une classe et l'action une méthode. En effet, si le contrôleur étend le kit (avec le mot-clé extends), alors pas besoin d'avoir d'argument $_this dans les actions, utiliser simplement $this aura le même effet.

Vers un MVC

Quand nous concevons une application, nous pouvons utiliser le modèle de conception Modèle-Vue-Contrôleur (ou MVC, pour Model-View-Controller). Vulgairement, ce modèle considère une application composée de trois couches : le modèle pour gérer les données (lire et écrire), la vue pour afficher des données et le contrôleur pour faire le lien entre les deux (le contrôleur utilise le modèle pour construire la vue). Plus ou moins de connexions peuvent exister entre les couches et le sens des connexions peut également différer d'une implémentation à une autre.

Ce que nous avons vu avec Hoa\Router et Hoa\Dispatcher est qu'il est facile de créer et d'adopter un tel modèle de conception, et qu'il est tout aussi facile de l'enrichir grâce au kit représenté par Hoa\Dispatcher\Kit. Par exemple, la bibliothèque Hoa\Console propose un dispatcheur avec un kit dédié à l'écriture de programme en ligne de commande avec des méthodes assistantes utiles pour des opérations répétitives (créer une aide, lister les entrées d'une commande, lire les options d'une commande etc. ; regrouper plusieurs outils dans le kit de manière intelligente). La couche contrôleur est immédiate avec le dispatcheur basic, la couche modèle n'a pour l'instant pas été abordée et la vue peut être n'importe quelle classe implémentant l'interface Hoa\View\Viewable. Toutefois, ces bibliothèques peuvent être utilisées dans une autre optique que le MVC grâce à un assemblage totalement différent.

Application : Gordon's blog

Nous allons maintenant mettre en pratique ce que nous venons d'apprendre en créant une petite application. Cette application est le blog du très célèbre Dr. Gordon Freeman.

Nous allons modifier le fichier Application/Public/index.php pour qu'il contienne le routeur HTTP et le dispatcheur vers des contrôleurs. Nous aurons deux actions possibles :

Pour cela, nous avons besoin de deux règles dans notre routeur pour nos actions respectives : disons i pour afficher la liste des articles et a pour afficher un article en particulier. La règle i sera accessible depuis le chemin de l'URI / et la règle a depuis /article-id.html. Nous allons avoir un contrôleur qui sera une classe avec deux méthodes, une pour chaque action :

$dispatcher = new Hoa\Dispatcher\Basic();
$router     = new Hoa\Router\Http();
$router->get('i', '/', 'blog', 'index')
       ->get('a', '/article-(?<id>\d+)\.html', 'blog', 'article');

$dispatcher->dispatch($router);

Nous avons dit que par défaut, le contrôleur était représenté par une classe Application\Controller\ControllerName. C'est pourquoi nous allons écrire la classe Application\Controller\Blog, avec deux méthodes : IndexAction et ArticleAction, respectivement pour la règle i et a. Ainsi :

namespace Application\Controller {

class Blog extends \Hoa\Dispatcher\Kit {

    public function IndexAction ( ) {

        echo 'Gordon\'s blog index.', "\n";
    }

    public function ArticleAction ( $id ) {

        echo 'Article n°', $id, '.', "\n";
    }
}

}

Nous allons maintenant tester notre application à l'aide de Bhoa en précisant que la racine est le dossier Public/ de notre application. Nous allons profiter de l'abstraction qu'offre le protocole hoa:// au lieu d'utiliser un chemin en dur :

$ myapp http:bhoa --root hoa://Application/Public

Puis, à l'aide de cURL ou d'un navigateur, nous allons joindre 127.0.0.1:8888 :

$ curl 127.0.0.1:8888/
Gordon's blog index
$ curl 127.0.0.1:8888/article-1.html
Article n°1
$ curl 127.0.0.1:8888/article-42.html
Article n°42

Tout fonctionne bien ! Mais ce ne sera pas toujours le cas. Par exemple, si nous appelons une URI qui n'est pas comprise par le routeur, une erreur sera levée :

$ curl 127.0.0.1:8888/foobar
Uncaught exception (Hoa\Router\Exception\NotFound):
Hoa\Router\Http::route(): (6) Cannot found an appropriated rule to route foobar.
in hoa://Library/Router/Http.php at line 490.

Plus précisément, une exception de type Hoa\Router\Exception\NotFound sera levée lors du dispatche. D'autres exceptions peuvent apparaître (comme Hoa\Router\Exception) mais ce ne sera pas à cause d'un problème de routage dû à l'utilisateur. Il serait intéressant de capturer cette exception et de réagir en conséquence, par exemple en indiquant qu'une erreur s'est produite :

try {

    $dispatcher->dispatch($router);
}
catch ( Hoa\Router\Exception\NotFound $e ) {

    echo 'Your page seems to be not found /o\.', "\n";
}

Et ainsi, plus d'exception affichée à l'utilisateur mais notre erreur :

$ curl 127.0.0.1:8888/foobar
Your page seems to be not found /o\.
menu