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
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 Interface Gateway), 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 huit au total :
CONNECT, DELETE, GET,
HEAD, OPTIONS, POST, PUT
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 :
from('Hoa')
-> import('Router.Http')
-> import('Dispatcher.Basic');
$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 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 :
- visibilité : ne pas en tenir compte pour ce chapitre ;
- méthodes HTTP : un tableau des méthodes qui permettent d'atteindre la
règle, parmi
GET,POST,PUT,DELETE,HEADetOPTIONS; - identifant : l'identifiant (unique) de la règle ;
- motif : une expression régulière définissant la forme de la règle ;
- call et able : composantes définissants une action que nous pouvons appeler ;
- variables : valeurs par défaut ou nouvelles variables pour les variables de la règle.
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 :
from('Hoa')
-> import('Router.Http');
$router = new Hoa\Router\Http();
$router->addRule($methods, $id, $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(array('get'), 'u', '/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 la base (comprendre, le préfixe) de notre
URI. Ses valeurs par défaut sont automatiques 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écisions que les constantes
Hoa\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 la base à chaque fois, nous pouvons utiliser la
méthode setBase dont le seul argument est la base.
À 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. Nosu 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 :
->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::setKitName. Par
exemple, nous créons un kit qui va ajouter une méthode
listRequestKeys se basant sur le routeur HTTP :
from('Hoa')
-> import('Dispatcher.Kit')
-> import('Dispatcher.Basic')
-> import('Router.Http');
class MyKit extends Hoa\Dispatcher\Kit {
public function listRequestKeys ( ) {
$theRule = $this->router->getTheRule();
return array_keys($theRule[Hoa\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 :
- afficher la liste des articles ;
- afficher un article en particulier.
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 :
from('Hoa')
-> import('Dispatcher.Basic')
-> import('Router.Http');
$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 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\.
