Développement web en Perl avec Mojolicious

par Julien Vehent - http://1nw.eu/!j

Cet article a ete initialement publie dans Gnu/Linux Magazine France #138 de Mai 2011.*

Cela fait maintenant quelques années que Perl, en tant que language de développement web, se fait discret. Beaucoup de développeurs l'utilisent toujours, mais la (relativement récente) vague Python/Ruby s'est ajoutée à PHP pour occuper l'essentiel du web. En tant que sysadmin, j'ai une tendance naturelle au conservatisme: si ça marche, ne touche à rien. Bon, évidemment, ça ne s'applique pas partout, mais quand il s'agit de programmation, j'aime bien me dire que Perl fera le boulot au moins aussi bien, et peut être mieux, que d'autres languages.

J'étais donc curieux du petit dernier de la communauté Perl: le framework MVC Mojolicious. Petit frère du très reconnu Catalyst, Mojolicious est un projet récent qui essaie de corriger certain mécontentement que les moines de Perl ont pu ressentir en utilisant Catalyst. Dans la mesure où les robes en bure, hormis le fait que ça gratte, ce n'est pas ma spécialité (et n'étant pas développeur de nature), cet article n'est pas un rapport d'expertise sur Perl et les frameworks MVC, mais plutôt une introduction au développement web avec Perl et Mojolicious 1.1.

1. L'indispensable Hello World

Très simple à afficher une fois que Mojolicious est installé. D'ailleurs cette installation est, elle aussi, très simple (c'est un des objectifs du framework). En passant par cpanminus (un module Perl qui s'occupe des étapes de téléchargement, configuration, installation automatiquement), la commande pour installer Mojolicious est aussi simple que

$ curl -L cpanmin.us | perl - Mojolicious --local-lib=/var/www/testmojo/mojolicious/

curl se connecte à cpanmin.us (ouvrez la page dans votre navigateur, ça pointe vers un module Perl hébergé sur github), récupère App::cpanminus et le fait exécuter par perl pour installer Mojolicious. L'option –local-lib permet d'installer Mojolicious dans un répertoire choisit par l'utilisateur. Vous pouvez également lancer cette commande sans spécifier de local lib, auquel cas il vous faudra être root et Mojolicious s'installera dans les répertoires du système.

Perl recherche les modules disponibles dans une liste de répertoire contenu dans la variable @INC. Essayez la commande perl -e 'foreach(@INC){print “$_\n”;}' pour afficher cette liste sur votre machine.

L'installation locale nous laisse avec 3 répertoires: bin, lib, et man. Le premier contient trois programmes (config_data, hypnotoad et mojo) dont nous aurons besoin un peu plus tard. Le second contient tous les modules de Mojolicious, et le troisième contient toute la documentation.

julien@arael:/var/www/testmojo$ tree -L 2 mojolicious/
mojolicious/
|---bin
|   |---config_data
|   |---hypnotoad
|   `---mojo
|---lib
|   `---perl5
|---man
    |---man1
    `---man3

Pour rester dans les bonnes pratiques, on va déplacer les modules dans un répertoire lib à la racine de la webapp.

julien@arael:/var/www/testmojo$ mv mojolicious/lib/perl5/ lib
 
$ tree lib/ -L 1
lib/
|--- CPAN
|--- ExtUtils
|--- inc
|--- Module
|--- Mojo
|--- Mojolicious
|--- Mojolicious.pm
|--- Mojo.pm
|--- ojo.pm
|--- Perl
|--- README.pod
|--- Test
`--- x86_64-linux-gnu-thread-multi

Il ne restera plus qu'à inclure ce répertoire 'lib' dans le code de la webapp.

La page de manuel de Mojolicious::Lite présente un exemple de “Hello World!” avec lequel nous allons démarrer.

$ man ./mojolicious/man/man3/Mojolicious::Lite.3pm
 
NAME
   Mojolicious::Lite - Micro Web Framework
 
[...]
Hello World!
A minimal Hello World application looks like this, strict and warnings are automatically enabled and a few functions imported when you use Mojolicious::Lite, turning your script into a full featured web application.
 
#!/usr/bin/env perl
use Mojolicious::Lite;
get '/' => sub { shift->render(text => 'Hello World!') };
app->start;
 
[...]

Le code ci-dessus utilise le module Mojolicious::Lite, une version simplifiée du framework, et démarre une application qui renvoie le texte 'Hello World!' dès qu'un client effectue un GET sur la page '/'.

1.1 Exécuter le code

Par habitude, mon premier réflexe en découvrant Mojolicious a été de monter un serveur Nginx avec FastCGI via spawn-fcgi et fcgiwrap, ma configuration habituelle pour exécuter du Perl/CGI. Mais ce n'est pas idéal. Le problème de cette configuration, c'est sa lenteur. Dans ma configuration de test, fcgiwrap ne parvenait pas à répondre a plus de 2,5 requêtes par secondes (sur un petit processeur, je vous l'accorde).

Hors, Mojolicious embarque toute une couche de communication, incluant les protocoles de transport que sont HTTP, CGI/FastCGI/PSGI et également un support des WebSockets. De fait, un programme utilisant Mojolicious peut directement lancer son propre serveur web (on verra plus tard comment contrôler cela avec Hypnotoad).

Si vous avez installé Mojolicious en local, pensez à inclure les librairies dans le script 'hello.pl' en ajoutant le code suivant en début de fichier:

use lib 'lib';

Lancez le serveur avec la commande suivante:

$ perl hello.pl daemon --reload
Sat Feb 26 15:14:45 2011 info Mojo::Server::Daemon:320 [7792]: Server listening (http://*:3000)
Server available at http://*:3000.

Et dirigez-vous vers http://localhost:3000/ pour admirer votre Hello World.

L'option –reload indique à Mojolicious de recharger automatiquement le code lorsqu'il est modifié. Ainsi, si vous changez “Hello World!” par “Bonjour Jean-Kevin…” et sauvegardez, le changement sera pris en compte sans relance du serveur.

Vous noterez également que la console dans laquelle le serveur est lancée affiche les logs de chaque requête. Pratique pour débugguer.

2. Modèle en couche

On l'a dit, Mojolicious est composée de plusieurs couches communiquant entre elles. La couche la plus basse est celle du transport, où l'on retrouve les protocoles réseaux. Ensuite vient Mojo. Mojo est le moteur de Mojolicious, il a été initialement écrit par Sebastian Riedel en 2008 pour améliorer Catalyst. Devant la réticence des mainteneurs de Catalyst à intégrer Mojo, Sebastian à écrit Mojolicious comme exemple d'utilisation, ne prévoyant alors pas que ce dernier grandirait au point de devenir un framework à part entière. Puis vient Mojolicious::Lite comme couche d'abstraction à Mojolicious. Toutefois, Mojolicious::Lite est entièrement basé sur Mojolicious et passer du framework allégé à la version complète n'implique pas de ré-écriture.

.-------------------------------------------.
|                                           |
|           .-------------------------------'
|           | .-----------------------------.
|Application| |       Mojolicious::Lite     |
|           | '-----------------------------'
|           | .-----------------------------.
|           | |          Mojolicious        |
'-----------' '-----------------------------'
.-------------------------------------------.
|                   Mojo                    |
'-------------------------------------------'
.---. .-------. .----. .--------. .---------.
|CGI| |FastCGI| |PSGI| |HTTP 1.1| |WebSocket|
'---' '-------' '----' '--------' '---------'

3. Une première application

En me lançant dans Mojolicious, je souhaitais avant tout répondre à un besoin: développer un raccourcisseur d'URL pour mon utilisation personnelle (des liens qui font moins de 20 charactères, ça passe mieux dans le corps d'un email). Et comme c'est un bon exercice, nous allons l'utiliser pour voir ce que Mojolicious a dans le ventre.

Créons un nouveau script nommé shorturl.pl qui va afficher une page d'accueil contenant un simple champ de formulaire:

#! /usr/bin/env perl
# mojo demo/raccourcisseur d'url/jve 20110200
use lib 'lib';
use Mojolicious::Lite;

#page d'accueil
get '/' => sub { 
    my $self = shift;
    return $self->render;
} => 'index';

app->start();

__DATA__
@@ layouts/default.html.ep
<!doctype html><html>
    <head><title>shorturl demo</title></head>
    <body><%= content %></body>
</html>

@@ index.html.ep
% layout 'default';
<%= form_for sendurl => (method => 'post') => begin %>
    <h1><a href="/">shorturl.local</a></h1>
    <p>URL :
    <%= text_field 'orig_url' %> 
    <%= submit_button 'Raccourcir' %>
    </p>
<% end %>

Le get '/' est similaire à celui que nous avons vu dans le Hello World, il traitera les requêtes HTTP GET à destination de la racine du site (c'est ce que Mojolicious appelle une “route”). Le code ne fait que de l'affichage via la méthode render. On nomme ensuite cette route 'index'.

Pour ceux qui sont familiers avec le modèle MVC, $self contient une instance du contrôleur (voir la page de man Mojolicious::Controller). Vous pouvez en afficher le contenu via le module Data::Dumper et la ligne print Dumper($self),“\n”;. C'est très instructif.

La commande app→start(); indique la fin du code de l'application et le lancement de celle-ci.

La section DATA en fin de fichier contient les templates à afficher. Un template démarre avec le mot clé @@ et continue jusqu'à ce qu'un autre template ne commence. Ici, le template utilisé sera “index.html.ep” qui se base lui même sur “default.html.ep”. Le template “index.html.ep” affiche un formulaire qui sera soumit à la route 'sendurl' (que nous n'avons pas encore créé) via la méthode HTTP POST. Ce formulaire prend un seul paramètre, nommé 'orig_url', qui contiendra donc l'url à raccourcir. Un formulaire commence par une section <%= […] begin %> et se termine avec <% end %>.

Lançons notre nouveau script pour visualiser le résultat:

$ perl shorturl.pl daemon --reload
Sat Feb 26 16:35:46 2011 info Mojo::Server::Daemon:316 [9325]: Server listening (http://*:3000)
Server available at http://*:3000.

3.1 Ajouter la route du Post

Si vous essayez de soumettre une URL dans le formulaire, Mojolicious vous répondra avec le message “Page not found, want to go home?”. C'est le comportement par défaut lorsque le framework ne connait pas la route à laquelle vous souhaitez accéder. Juste après le code de la page d'accueil, et avant 'app→start();', nous allons donc ajouter la déclaration de la route 'sendurl' avec la portion de code qui suit :

# reception d'une URL
post '/sendurl' => sub {
    my $self = shift;

    # controler la validite de l'url
    my $url =  Mojo::URL->new($self->param('orig_url'));
    if(!$url->is_abs){
        return $self->redirect_to('index');
    }

    # store_url retourne l'url raccourcie
    my $short_url = store_url($self->param('orig_url'));

    # afficher une page de confirmation
    return $self->render('confirm', shortened => $short_url, host => $self->req->url->base->host, port => $self->req->url->base->port);
};

Le contrôleur ($self) contient les paramètres passés à la route. Quand le navigateur du client a soumit le formulaire, il a renvoyé l'URL à raccourcir dans la variable 'orig_url', et l'on accède à cette variable dans le code via $self→param('orig_url').

La route 'sendurl' traite uniquement des requêtes Post. On contrôle tout d'abord la validité du paramètre 'orig_url' via le module Mojo::URL et redirige le client vers la page d'accueil si le paramètre n'est pas une URL valide (notez que $url→is_abs est fournit par Mojo::URL, qui est une fonction de la couche inférieure Mojo). Si l'URL est valide, la fonction 'store_url' est appelée avec l'url en paramètre.

Quand 'store_url' a terminé, la valeur de l'URL courte est retournée, et l'on renvoi cela au client dans une page de confirmation. La ligne 'render' est un peu plus complexe que précédemment, car on spécifie cette fois le nom du template à utiliser (confirm qui utilisera “confirm.html.ep”) ainsi que des paramètres à passer au template pour construire la page (l'url courte dans 'shortened' et l'adresse 'host' et port 'port' du serveur).

Le template ajouté à la fin du script est le suivant:

@@ confirm.html.ep
% layout 'default';
    <h1><a href="/">shorturl.local</a></h1>
    <p>Votre adresse a ete enregistree</p><br>
    <% if ($port == 80) { %>
        <p>URL courte : <a href="http://<%=$host%>/<%=$shortened%>">http://<%=$host%>/<%=$shortened%></a></p><br>
    <%} else { %>
        <p>URL courte : <a href="http://<%=$host%>:<%=$port%>/<%=$shortened%>">http://<%=$host%>:<%=$port%>/<%=$shortened%></a></p><br>
    <% } %>

    <a href="/">retour</a>

Ce template est intéressant car on voit ici comment Mojolicious traite l'affichage de paramètres dans les templates. La syntaxe <% %>, similaire à celle vue dans la définition du formulaire, permet ici d'exécuter un test if{}else{} sur la valeur du port (si port égal 80, ne pas l'afficher). De plus, à la ligne 'URL courte', chaque section comprise dans <%= %> est interprétée comme une expression Perl et permet donc d'afficher des variables. La page de manuel Mojolicious::Guides::Rendering vous en dira plus, mais dans les grandes lignes, on intègre du Perl dans les templates avec <% %> et des variables avec <%= %>.

Les paramètres sont transmis de la fonction au template au travers du Stash, une sorte de fourre-tout qui fonctionne comme un hash et dans lequel vous pouvez stocker ce que vous voulez. Le Stash du contrôleur est valide le temps de traiter une requête et de renvoyer la réponse, mais il est également possible de manipuler un stash au niveau de l'application qui sera persistant entre les requêtes. En toute franchise, c'est la partie de Mojolicious qui m'a donné le plus de mal au crâne, et il vous faudra certainement quelques heures de lecture de la documentation et de questions sur l'IRC avant de manipuler tout cela correctement.

Pour le moment, contentons nous de peu: le stash permet de transmettre des variables au template.

Il ne nous reste plus qu'à placer une fonction 'store_url' au début du script. Cette fonction créée une URL courte (un timestamp unix en hexadécimal) et stocke l'association “<url courte>|<url originale en base64>” dans un fichier nommé 'urls.txt'.

use MIME::Base64;

sub store_url{
    my ($url_orig) = @_;
    chomp $url_orig;
    my $b64_url = encode_base64($url_orig, "");

    my $short_url = "!".sprintf("%x",time);

    open(STORAGE, ">> urls.txt") or die "erreur d'acces au stockage\n$!";
    print STORAGE "$short_url|$b64_url\n";
    close STORAGE;

    return $short_url;
}

Voilà pour l'ajout d'URL. Le résultat, c'est une fonction qui ajoute une URL dans un fichier local et renvoi la page suivante:

shorturl.local
Votre adresse a ete enregistree
URL courte : http://localhost:3000/!4d5efcd4
retour

Le fichier “urls.txt” contient quand à lui notre ligne <url courte>|<url longue>:

$ cat urls.txt 
!4d5efcd4|aHR0cDovL3dpa2kubGludXh3YWxsL....

3.2 Manipuler le routage

Une route est, nous l'avons vu, une version améliorée d'un chemin. Mojolicious permet une manipulation avancée des routes sous la forme de placeholder (un emplacement à remplacer). C'est en fait une autre représentation des variables d'URL à la manière d'un PATH_INFO en CGI. En pratique, si jean-kevin accède à son compte via l'adresse http://www.example.net/profile/jeankevin, alors nous pourrons capturer son nom d'utilisateur dans Mojolicious avec la route suivante:

get '/profile/:userid' => [userid => qr/[a-z0-9]{0,30}/] => sub {
    my $self = shift;
    my $userid = $self->param('userid');
    $self->render(text => "Hello $userid");
};

Le code ci-dessus capture la partie ':userid' de l'URL, qui sera donc le placeholder, et stocke ce placeholder dans le paramètre 'userid'. En reprenant notre exemple précédent, 'userid' contient donc 'jeankevin' et l'on peut réutiliser cette valeur autant que l'on veut jusqu'à ce qu'une réponse soit renvoyée au client. On remarquera qu'une expression régulière est présente dans la déclaration de la route, elle est optionnelle, mais permet de contrôler les données en entrées directement dans la définition des routes. C'est simple, et ça fait plaisir à votre ingénieur sécurité.

Selon le même mécanisme, nous allons définir une route qui va capturer toutes les URLs démarrant avec un point d'exclamation '!' après le premier slash. Ces URLs vont correpondre à des URLs courtes pour lesquelles il faudra donc rechercher la correspondance en URLs originales, et renvoyer un redirect (HTTP 302) au client.

get '/:shorturl' => ([shorturl => qr/\![a-f0-9]{8}/]) => sub {
    my $self = shift;
    
    my $redirect_url = get_url($self->param('shorturl'));
    if($redirect_url ne "-1"){
        return $self->redirect_to($redirect_url);
    }
    return $self->redirect_to('index');
};

On vérifie que le placeholder 'shorturl' a un format sur 8 caractères hexadécimaux plus un point d'exclamation au début. La route appelle ensuite une fonction get_url qui va retourner l'URL originale. Reste ensuite a renvoyer un redirect au client. Si une étape ne se passe pas bien, le client est redirigé vers la page d'accueil (que nous avons nommée 'index' au début).

La fonction get_url parcourt simplement le fichier “urls.txt” pour trouver la ligne correspondante.

sub get_url{
    my $self = shift;
    my ($short_url) = @_; 
    open(FILE, "urls.txt") || die;
    # lecture non bloquante
    flock(FILE, 4); 
    while(<FILE>){
        if ($_ =~ /$short_url/){
            my @url_line = split(/\|/,$_);
            return decode_base64($url_line[1]);
        }   
    }   
    close(FILE);
    #url non trouvee, retourne -1
    return -1; 
}

Ces deux fonctions ajoutées, vous pouvez tester l'ajout et la redirection de vos URLs, le tout en moins de 150 lignes de Perl et de Mojolicious. Il reste toutefois beaucoup à faire avant d'appeler cela une application web.

4. Découpage et organisation

4.1 Templates

Pour le moment, tout le code est placé dans un seul fichier. Ce n'est évidemment pas idéal. En fait, seul le controller est censé être présent dans le fichier central. Cela correspond à la gestion des routes et des requêtes/réponses entre le client et l'application. Le reste peut être organisé dans un ensemble de répertoires, comme décrit dans la page de manuel Mojolicious::Guides::Growing.

myapp                      # Répertoire de l'application
|- script                  # Répertoire des scripts
|  `- myapp                # script de l'application
|- lib                     # Répertoire des bibliothèques
|  |- MyApp.pm             # Classe de l'application
|  `- MyApp                # Espace de nommage
|     `- Example.pm        # Classe de contrôleur
|- t                       # Répertoire des tests
|  `- basic.t              # un test
|- log                     # Répertoire des logs
|  `- development.log      # Logs de développemt
|- public                  # Répertoire des contenus statiques
|  `- index.html           # Fichier HTML statique
`- templates               # Répertoire des templates
   |- layouts              # Répertoire de template pour les mise en page
   |  `- default.html.ep   # Template de mise en page par défaut
   `- example              # Répertoire de template pour le contrôleur "example"
      `- welcome.html.ep   # Template pour l'action 'welcome'

Pour les templates, par exemple, on va supprimer la section DATA de shorturl.pl et placer chaque template dans un fichier séparé dans les répertoires templates et layouts correspondants, pour arriver au découpage ci-dessous:

$ tree templates/ -L 2
templates/
|--- confirm.html.ep
|--- index.html.ep
`--- layouts
    `- default.html.ep

4.2 Packages

De même que pour les templates, il est souhaitable de séparer la logique du contrôleur du coeur de l'application. On va donc créer un package ShortURL.pm qui va contenir les fonctions get_url() et store_url(). Ce package est créé dans le répertoire 'lib' de l'application, comme suis:

package ShortURL;
use MIME::Base64;

sub get_url{
    my $self = shift;
    my ($short_url) = @_;
    open(FILE, "urls.txt") || die;
    # lecture non bloquante
    flock(FILE, 4);
    while(<FILE>){
        if ($_ =~ /$short_url\|/){
            my @url_line = split(/\|/,$_);
            return decode_base64($url_line[1]);
        }
    }
    close(FILE);
    #url non trouvee, retourne -1
    return -1;
}

sub store_url{
    my $self = shift;
    my ($url_orig) = @_;
    chomp $url_orig;
    my $b64_url = encode_base64($url_orig, "");

    my $short_url = "!".sprintf("%x",time);

    open(STORAGE, ">> urls.txt") or die "erreur d'acces au stockage\n$!";
    print STORAGE "$short_url|$b64_url\n";
    close STORAGE;

    return $short_url;
}

1;

Il faut noter deux choses dans le code précédent. Tout d'abord, nos fonctions vont maintenant être appelée en mode Objet, et de fait un appel de fonction sous la forme package→subfunc(@args) passera les arguments (“subfunc”, @args). Comme “subfunc” ne nous intéresse pas, on le stocke dans $self et on passe au reste.

Deuxièmement: notez le “1;” à la fin du code. Il est nécessaire car Perl requiert qu'un module retourne “true” (donc 1), même lorsqu'il est composé uniquement de sous-fonctions.

Dans le contrôleur maintenant, il faut importer le module ShortURL.pm et également modifier les appels des fonctions. On n'appelera plus get_url() mais ShortURL→get_url(). Le code source (tronqué) de shorturl.pl est donc le suivant:

[...]
post '/sendurl' => sub {
	[...]
    # store_url retourne l'url raccourcie
    my $short_url = ShortURL->store_url($self->param('orig_url'));
[...]

get '/:shorturl' => ([shorturl => qr/\![a-f0-9]{8}/]) => sub {
    [...]
    my $redirect_url = ShortURL->get_url($self->param('shorturl'));
[...]

Après ces opérations, on a la logique applicative dans un module externe, et le contrôleur dans le code principal. C'est tout de même plus propre.

5. Utiliser les logs

Nous avons vu, au tout début, que Mojolicious renvoi les logs dans le terminal qui lance le programme. Cela est vrai, sauf si un répertoire log existe. Auquel cas, les logs y sont placés, classés par environnement d'exécution. Par défaut, nous utilisons l'environnement de développement, et log contiendra donc un fichier development.log avec tous les logs.

Note: pour changer de mode, il suffit de rajouter l'option –mode production au lancement du programme, mais dans ce cas, aucun log n'est produit par défaut.

Les logs par défaut sont utiles pour détecter les problèmes, mais ne donnent pas d'informations sur les fonctions exécutées, les clients, etc… Pour cela, Mojolicious intègre l'appel app→log dans lequel nous allons passer toutes sortes d'informations à conserver dans les logs.

5.1 Gérer les niveaux

Si nous souhaitons loguer l'accès à la page d'accueil, la fonction suivante dans la route '/' suffit:

#page d'accueil
get '/' => sub { 
    my $self = shift;
    app->log->error("appel de la page d'accueil");
    return $self->render;
} => 'index';

app→log→error va écrire la phrase “appel de la page d'accueil” dans le fichier de log correspondant au mode development/production.

$ cat production.log 
Sat Feb 26 18:53:20 2011 error Mojo::Loader:16 [12514]: appel de la page d'accueil

Toutefois, si cette information n'est pas intéressante en production, on peut simplement utiliser le niveau de log debug:

app->log->debug("appel de la page d'accueil");

Et dans ce cas, le log ne sera enregistré qu'en mode de développement.

Il y a en tout 5 niveaux de logs qui peuvent être utilisé:

app->log->debug("niveau de log par defaut pour le développement");
app->log->info("niveau intermédiaire");
app->log->warn("encore un niveau intermédiaire");
app->log->error("niveau de log par défaut pour la production");
app->log->fatal("Ouch !");

Avec cela, il est simple de placer des logs de debug partout dans le code, sans pour autant surcharger les environnements de production avec des traces inutiles.

5.2 Utiliser un "helper"

Mojolicious va plus loin dans la gestion des logs. Il est possible de créer une fonction générique (un “helper”) permettant de produire une ligne de log, et d'appeler ensuite cette fonction chaque fois que l'on souhaite loguer quelque chose. La fonction log_req ci-dessous permet de loguer les informations du client liées à la requête en cours. On définit cet helper comme une fonction à part du contrôleur (donc dans shorturl.pl) comme suis:

app->helper(log_req => sub {
    my $self  = shift;
    my $method = $self->req->method;
    my $url = $self->req->url;
    my $version = $self->req->version;
    my $ip    = $self->tx->remote_address;
    return "Requete recue => $method $url HTTP/$version depuis $ip";
});  

L'helper est ensuite accessible dans le contrôleur à l'emplacement $self→log_req. Ensuite, pour chaque route, on ajoute un log qui appelle notre helper pour produire la ligne de log. En choisissant le niveau de log “error”, on s'assure que cette information sera présente en production et en développement:

app->log->error($self->log_req);

Ce qui produit la ligne suivante:

$ tail -n 1 production.log 
Sat Feb 26 19:13:30 2011 error Mojo::Loader:25 [12514]: Requete recue => GET / HTTP/1.1 depuis 127.0.0.1

6. Définir un fichier de configuration

Jusqu'ici, nous avons conservé certaines valeurs en dur dans le code. Hors, Mojolicious permet de maintenir un fichier de configuration au format JSON, et de lire ce fichier de configuration au démarrage de l'application, ou dans un package. Les paramètres de configuration seront ensuite accessibles dans une structure en mémoire (le Stash de l'application).

Lors de la phase de découpage, nous avons créé un package ShortURL contenant le corps de l'application. C'est dans ce package que nous allons lire le fichier de configuration. Mojolicious fournit un plugin du nom de 'json_config'. Par défaut, ce dernier essayera de lire un fichier de configuration à la racine portant le même nom que l'application (dans notre cas, “shorturl.json”). Définissons l'emplacement du stockage des URLs dans ce fichier:

$ vim shorturl.json
{
    "storage_file" : "urls.txt"
}

Maintenant, dans lib/ShortURL.pm, il est possible de remplacer les occurences de “urls.txt” dans les fonctions get_url et store_url par la valeur prise dans le fichier de configuration:

package ShortURL;
use MIME::Base64;
use Mojolicious::Lite;

# chargement du plugin, lit automatiquement
# le fichier de configuration dans "shorturl.json"
my $config = plugin 'json_config';

sub get_url{
    [...]
    app->log->debug("get_url: acces au fichier de stockage \'$config->{storage_file}\'");

    open(FILE, "$config->{storage_file}")
        or die "erreur d'acces a \'$config->{storage_file}\'\n$!";
[...]

sub store_url{
    [...]
    app->log->debug("store_url: acces au fichier de stockage \'$config->{storage_file}\'");

    open(STORAGE, ">> $config->{storage_file}")
        or die "erreur d'acces a \'$config->{storage_file}\'\n$!";
[...]

7. Réaliser des tests

Un des points intéressant du framework est la possibilité de préparer des tests unitaires, et de lancer ces derniers en une ligne de commande. En se basant sur le module Test::More (dont vous trouverez une excellente présentation dans le livre Modern Perl, disponible en ligne) et sur Mojo::Test, on peut déclarer un test qui va envoyer une requête et évaluer la réponse renvoyée par l'application.

Imaginons un test simple: l'URL courte '/!abcd1234' doit renvoyer une redirection (HTTP 302) vers le site http://test_mon_appli.example.net. Nous allons tout d'abord ajouter cette ligne à la liste des URLs dans le fichier de stockage:

!abcd1234|aHR0cDovL3Rlc3RfbW9uX2FwcGxpLmV4YW1wbGUubmV0

Les tests doivent être placés dans un répertoire “t” à la racine de l'application. Pour ajouter un test sur notre redirection, nous allons créer le fichier “t/shorturl.t” qui contient le code suivant:

use Test::More tests => 3;
use Test::Mojo;

# FindBin permet de trouver le script de l'appli
use FindBin;
$ENV{MOJO_HOME} = "$FindBin::Bin/../";
require "$ENV{MOJO_HOME}/shorturl.pl";

# declaration du test
my $t = Test::Mojo->new;
$t->get_ok('/!abcd1234')
    ->status_is(302)
    ->header_is(Location =>'http://test_mon_appli.example.net');

Le test get_ok va ouvrir l'URL '/!abcd1234' et vérifier que la réponse est bien de type HTTP 302, et qu'elle contient bien l'en-tête Location: http://test_mon_appli.example.net.

Mojolicious va automatiquement rechercher les tests dans le fichier “t/<appli>.t”. Donc en lancant la commande suivante, on exécute tous les tests:

$ perl shorturl.pl test
Running tests from '/var/www/testmojo/t'.
t/shorturl.t .. ok   
All tests successful.
Files=1, Tests=3,  1 wallclock secs ( 0.02 usr  0.01 sys +  0.24 cusr  0.03 csys =  0.30 CPU)
Result: PASS

On pourrait également ajouter un test sur l'envoi d'URL via le formulaire. Ce test ressemblerait alors au code suivant:

$t->post_form_ok('/sendurl' => {orig_url => 'http://test.example.net'})
    ->status_is(200)
    ->header_is('Content-Type' => 'text/html;charset=UTF-8');

8. Un peu de couleur

Avec une application désormais fonctionnelle, la dernière étape est de rendre tout cela un peu plus agréable à l'oeil, en utilisant une CSS et une image avec les templates. On en a parlé un peu plus tôt: le répertoire pour les contenus statiques est “./public”, qu'il faut donc créer. Ce dernier va contenir une CSS simple dans public/style.css et un logo dans public/lnwcrab.gif. La CSS est la suivante:

/* shorturl - css de base */
body{
    background-color: #e9eae8;
}
#central{
    width:50%;
    margin: auto;
    padding: 20px;
    background-color: #900;
    color: white;
}
#central h1, a:link, a:visited {
    color: white;
    text-decoration: none;
}
#central img {
    float: right;
    position: fixed;
    margin-left: 40%;
}

Le contenu du répertoire “public” est automatiquement servi par Mojolicious comme s'il étant présent à la racine. Donc pour accèder à notre CSS, il faut aller à l'adresse 'http://localhost:3000/style.css' (et non pas /public/style.css).

Editons maintenant les templates. Tout d'abord, le layout par défaut va inclure la CSS dans les en-têtes de la page, comme suis:

$ cat templates/layouts/default.html.ep 

<!doctype html><html>
    <head>
        <title>shorturl demo</title>
        <link type="text/css" rel="stylesheet" media="all" href="style.css" />
    </head>
    <body><%= content %></body>
</html>

Ce layout étant inclus dans tous les templates, on peut maintenant utiliser l'ID “central” dans les pages (je ne met que le template de l'index, étant donné que c'est pareil partout).

$ cat templates/index.html.ep 

% layout 'default';
<div id="central">
<%= form_for sendurl => (method => 'post') => begin %>
    <img src="lnwcrab.gif" width=100 />
    <h1><a href="/">shorturl.local</a></h1>
    <p>URL :
        <%= text_field 'orig_url' %><br />
        <%= submit_button 'Raccourcir' %>
    </p>
<% end %>
</div>

Ce qui nous donne le résultat en capture d'écran. Un petit passage par le validator.w3c.org permet également de vérifier que tout cela est HTML5 ready (même si les vérifications ne sont encore qu'expérimentales).

9. Hypnotoad et le passage en prod

Avant de conclure, un petit mot sur le serveur web embarqué de Mojolicious. Jusque là, nous avons exécuté le script directement, mais Hypnotoad nous permet un contrôle plus fin de l'exécution, et de très bonnes performances (entre autres grâce au copy on write, au support de Epoll et Kqueue, …).

Hypnotoad prend en paramètre un fichier de configuration, dont la syntaxe est expliquée dans la page de manuel Mojo::Server::Hypnotoad. Pour faire simple, la configuration suivante nous permet de lancer hypnotoad en écoute sur deux ports, l'un étant limité à localhost. Une petite astuce permet de démarrer deux fois plus de workers qu'il n'y a de processeurs dans /proc/cpuinfo, un ratio recommandé par les développeurs. Le reste est classique: file d'attente de connexions et uid/gid du processus.

{
    # adresses et ports en écoute
    listen => ['http://*:8080', 'http://127.0.0.1:3000'],

    # nb de processus basée sur le nombre de CPU x 2
    workers => (`grep processor /proc/cpuinfo|wc -l` * 2),

    # taille de la file d'attente de connexions, par worker
    accepts => 1000,

    # propriétaire et groupe du serveur
    user => 'julien',
    group => 'julien'
};

Le serveur se lance alors avec la commande suivante suivante:

$ ./mojolicious/bin/hypnotoad --config hypnotoad.conf shorturl.pl 
Server available at http://*:8080.
Server available at http://127.0.0.1:3000.

Par défaut, hypnotoad place son pid dans hypnotoad.pid à la racine de l'application.

Une autre fonction intéressante d'hypnotoad est de permettre le rechargement à chaud. Il suffit d'envoyer un signal USR2 au processus hypnotoad et ce dernier va recharger le code, sans se relancer.

$ kill -s 'USR2' `cat hypnotoad.pid`

9.1 Et ça va vite ?

Plutôt vite oui ! Sur ma machine de test, un petit Atom D510, Mojolicious répond aux GET avec une moyenne stable de 250 hits par secondes et consomme un peu plus de 12Mo de mémoire résidente par worker. Evidemment, notre application de test est bien trop limitée pour prouver une supériorité par rapport à d'autres framework. Mais pour un début, c'est déjà satisfaisant.

10. Et pour finir

Notre balade d'introduction dans le joyeux monde de Mojolicious est terminée, et l'on a seulement survolé les possibilités offertes. Pour aller plus loin, je vous recommande la lecture de la documentation officielle, ainsi que du wiki et de quelques applications (sur github: gadwall - http://1nw.eu/!l0 - et undeadly - http://1nw.eu/!BA). Le code source de notre application est disponible à l'adresse http://1nw.eu/!mojo et si vous voulez une version plus avancée à installer chez vous, recherchez 1nw sur github.

Ce framework a clairement un bel avenir devant lui, et amène un peu d'air frais dans le monde de Perl. Il n'est pas exempt de défauts (la non ré-utilisation de nombreux modules CPAN fait débat), mais offre un environnement solide, performant et relativement simple pour le développement d'applications web.

fr/ressources/dossiers/mojolicious.txt · Last modified: 2011/07/16 17:22 (external edit)
CC Attribution-Noncommercial-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0