Créer une application Perl autour de MySQL : Intégration avec Mojolicious, HTML::Tiny et HTML::FormHandler (3/3)

GNU/Linux Magazine n° 169 | mars 2014 | Dominique Dumont
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Reprenons l'histoire du hacker à qui on a demandé de créer des pages web pour le suivi de la production logicielle de la société. Comme tout hacker qui se respecte, il préfère que la machine fasse son boulot à sa place. Pour ce faire, il a mis en place une base de données MySQL, un ORM (DBIx:Class). Reste maintenant à fournir un serveur web pour exploiter les données de cette base.

Petit rappel des épisodes précédents : notre hacker favori a expliqué comment mettre en place dans une base de données MySQL les tables et relations qui assureront la persistance des données de production. Puis, comment lire et écrire ces données avec DBIx:Class. Le code SQL de la structure de la base et le petit script contenant les exemples d'accès à la base sont disponibles dans un dépôt git [GITHUBADOD].

1. Exploitation des données

Maintenant que nous savons écrire des données dans la base, nous allons voir comment récupérer ces données pour les afficher dans des pages HTML avec Mojolicious::Lite. Ce module fournit une infrastructure « légère » de serveur web. Ceci permet de tester rapidement les pages produites avec le mini-serveur de Mojolicious. Comme ce module a été décrit précédemment [GLMF138], les explications sur Mojolicious seront rapides.

1.1 Mise en place d'un micro serveur avec Mojolicious

Pour démarrer plus facilement, on va créer un petit script avec Mojolicious::Lite qui sera chargé de lire les informations de la base de données. On verra plus tard comment mettre à jour ces données.

La commande suivante va créer un embryon de ce script :

$ mojo generate lite_app rapport.pl
$ cat rapport.pl
#!/usr/bin/env perl
use Mojolicious::Lite;
# Documentation browser under "/perldoc"
plugin ‘PODRenderer’;
get ‘/’ => sub {
my $self = shift;
$self->render(‘index’);
};
app->start;
__DATA__
@@ index.html.ep
% layout ‘default’;
% title ‘Welcome’;
Welcome to the Mojolicious real-time web framework!

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body><%= content %></body>
</html>

En résumé, la première partie de ce script déclare comment traiter la route / avec le template index. Celui-ci est décrit dans le script après la ligne __DATA__. Si ce résumé vous laisse perplexe, je vous invite à lire [GLMF138].

Pour tester le résultat, lancez le mini-démon :

$ ./rapport.pl daemon
[Fri Jun 1 16:24:43 2012] [info] Listening at "http://*:3000".
Server available at http://127.0.0.1:3000.

Et passez l'URL indiquée à votre butineur favori. Vous y verrez affichée une petite page d'accueil Mojolicious. En bonus, grâce au plugin Podrenderer, vous pouvez afficher des pages de doc de Perl sous l'URL http://localhost:3000/perldoc/. Par exemple, http://localhost:3000/perldoc/perlre.

Vous pouvez aussi lancer le script avec :

$ morbo rapport.pl daemon

morbo surveille les modifications du fichier rapport.pl et va le recharger si nécessaire. C'est très pratique dans les phases de développement.

2. Extraction et formatage des informations de la base

Revenons-en à nos produits. On a d'abord besoin d'afficher la liste de produits disponibles sous forme de table générée à partir du contenu de la base de données. Cette section va détailler la construction du script Mojolicious.

2.1 Utilisation des templates Mojolicious pour la liste des produits

D'abord, il faut se connecter à la base :

use Mojolicious::Lite;
use Integ::Schema ;
my $schema = Integ::Schema->connect(
‘DBI:mysql:database=Integ;host=localhost;port=3306’,
‘integ_user’, ‘’, { RaiseError => 1 }
);

Ensuite, on gère la route / pour que l'URL http://localhost:3000 renvoie la liste des produits. render va utiliser le template product_list pour générer le code HTML. Le reste des paramètres est un ensemble de clés-valeurs utilisables dans le template :

get ‘/’ => sub {
my $self = shift;
$self->render(product_list => rs => $schema-> resultset(‘Product’) );
};

Cette dernière ligne démarre effectivement le daemon :

app->start;

Avec Mojolicious::Lite, les templates sont situés dans la section data (après la balise __DATA__). La syntaxe du template est détaillée dans [GLMF138]. En résumé :

- Les balises <% ... %> et les lignes commençant par % sont du code Perl ;

- Les balises <%= ... %> sont du code Perl dont la sortie est injectée dans le template, après une transformation pour le rendre moins lisible, mais compatible avec XML (e.g. > est changé en &gt;) ;

- les balises <%== ... %> sont du code Perl dont la sortie est injectée verbatim dans le template. Ce qui est indispensable si le code Perl génère lui-même du HTML.

Le code suivant (en zone DATA) va utiliser le « layout » défini par la commande mojo generate et renvoyer une page HTML contenant un tableau avec les listes des produits disponibles.

__DATA__
@@ product_list.html.ep
% layout ‘default’ ;
% title ‘Product list’ ;
<h1>Produits</h1>
<table>
<tr>
<th>Produit</th>
<th>Home page</th>
</tr>
% while (my $product = $rs->next) {
% my $name = $product-> name ;
<tr>
<td><a href="<%==$name%>"><%== $name %></td>
<td><a href="<%== $product->home_page%>">home page</td>
</tr>
% }
</table>

Notez que la première URL du tableau est construite sous la forme <nom_du_produit>. Il faudra définir une route dans le script Mojolicious pour répondre à cette adresse.

Pour tester, il faut relancer le mini-serveur (sauf si morbo s'en charge) et recharger la page web. Et là, horreur, cette page est moche : les informations sont là, mais en noir sur fond blanc.

Pour remédier à cette faute de goût, il suffit d'ajouter une feuille de styles dans le « layout » :

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link href="/css/my_style.css" rel="stylesheet" />
</head>
<body><%= content %></body>
</html>

Plutôt que d'ajouter une route dans la section DATA, on peut placer le fichier CSS dans le répertoire public/css, Mojolicious::Lite saura aller le chercher à cet endroit.

2.2 Mais c'était quoi cette horreur ?

Vous aurez sans doute remarqué que le template product_list.html.ep du paragraphe précédent n'est pas forcément agréable à lire : c'est un pot-pourri indigeste de balises HTML et de balises de template, persillé avec du code Perl.

C'est une limite des templates : quelques variables Perl de-ci de-là ne posent pas de problème, mais ça devient illisible dès que le traitement métier devient important par rapport au texte.

La philosophie de Perl étant d'avoir plus d'une façon de faire une chose [TIMTOWTDI], on va utiliser pour les autres routes une façon alternative. Tous ces traitements complexes seront écrits principalement avec du code Perl et le code HTML sera généré avec le module qui va bien. Par exemple, le module HTML::Tiny.

2.3 Une petite digression sur HTML::Tiny

HTML::Tiny est un module bien pratique pour générer de l'HTML. Il fournit une méthode pour chaque balise HTML. Chaque méthode renverra une liste ou une chaîne, selon le contexte.

Par exemple, on peut avoir :

my $h = HTML::Tiny->new;
my $html_text = ‘’;
$html_text .= $h->h1(‘Une petite digression...’) ;

Les attributs d'une balise peuvent être passés dans un hash ref :

$html_text .= $h->div({class => ‘perl_module’},’HTML::Tiny’ ) ;

Chaque paramètre va générer une balise :

say $h->td(qw/foo bar baz/) ;
# <td>foo</td><td>bar</td><td>baz</td>

On peut aussi avoir une balise pour une liste en passant une référence :

my @chanson = map {"$_ km à pied, ça use, ça use... "} qw/un deux trois/ ;

my @li_en_vrac = $h->li( @chanson ) ;

say $h->ul( \@li_en_vrac ) ;

Ce qui donne, après reformatage manuel :

<ul>

<li>un km à pied, ça use, ça use... </li>

<li>deux km à pied, ça use, ça use... </li>

<li>trois km à pied, ça use, ça use... </li>

</ul>

Si une balise n'est pas implémentée dans HTML::Tiny, la méthode tag devra être utilisée :

say $h->tag('titi', qw/foo bar baz/),"\n";'

# <titi>foo</titi><titi>bar</titi><titi>baz</titi>

2.4 Générer la liste des versions

Le principe est le même que pour générer la liste de produits. Plutôt que d'utiliser les templates Mojolicious pour écrire la table, on va utiliser HTML::Tiny.

Tout d'abord, on crée un objet HTML::Tiny une fois pour toutes et on déclare la route. Celle-ci est un peu plus compliquée, car elle contient le nom du produit sous la forme :name.

my $h = HTML::Tiny->new;

get '/:name' => sub {

my $self = shift;

my $p_rs = $schema -> resultset('Product');

On récupère le nom du produit avec la méthode param et on recherche dans la base de données les infos pour les récupérer dans un objet Schema::Product :

my $n = $self->param('name') ;

my $p_obj = $p_rs->search({name => $n }) ->single ;

Et on construit le tableau HTML en faisant une itération sur les objets Schema::ProductVersion renvoyés par $p_obj->product_versions->all :

my @rows ;

foreach my $version_obj ($p_obj->product_versions->all) {

my $version_as_string = $version_obj->version ;

Le lien vers la page dédiée à la version est créé avec la méthode a. Cette URL sera de la forme /<nom_du_produit>/<version> :

my $v_link = $h->a({href => $version_as_string }, $version_as_string) ;

Les lignes du tableau sont créées avec :

my @data = $h->td( $v_link , $version_obj->date->ymd ) ;

push @rows, $h->tr( \@data ) ;

}

Et enfin, on passe le tableau HTML sous forme de string au template version_list :

$self->render ( version_list => rows => join("\n",@rows), name => $n ) ;

} ;

Comme tout le boulot est fait avec HTML::Tiny, le template version_list devient beaucoup plus simple :

@@ version_list.html.ep

% layout 'default' ;

% title 'Product version list' ;

<h1>Versions du produit <%== $name %></h1>

<table>

<tr>

<th>Version</th>

<th>Date</th>

</tr>

<%== $rows %>

</table>

Le script correspondant est disponible sur mon dépôt git [GITHUBADOD] sous rapport.pl.

3. Modification des données avec Mojolicious

Pour assurer le suivi de la qualification du produit, on va ajouter un formulaire web pour pouvoir modifier facilement le statut d'une version d'un produit. Pour cet article, ce statut est directement tiré des pratiques Debian, soit « unstable », « testing » ou « unstable ».

Le but est de créer un formulaire qui permettra de modifier le statut d'une version de produit.

D'abord, il faut une route pour lister les versions d'un produit. Chaque version contient un lien qui pointe vers une page d'édition. Cette route se contente de récupérer l'objet produit de la base pour le passer au template :

get '/:name' => sub {

my $self = shift;

# on récupère l'objet dans la base

my $p_obj = $schema

-> resultset('Product')

-> find({name => $self->stash('name')}) ,

# et on le passe au template

$self->render (

template => 'mojo_prod_v_list',

p => $p_obj

) ;

} ;

Le template en question est assez simple :

@@mojo_prod_v_list.html.ep

% layout 'default' ;

% title 'Product versions' ;

<h1>Versions of product <%== $p->name %></h1>

<ul>

% foreach my $r ($p->product_versions) {

% my $v = $r->version ;

<li><a href="<%== $p->name.'/'.$v %>"><%== $v %></a>

% }

</ul>

Ensuite, on crée la route pour éditer une version du produit. Comme le numéro de version contient un ., il faut utiliser #version au lieu de :version. Les boutons sont construits ici pour simplifier le traitement en Perl dans le template :

get '/:name/#version' => sub {

my $self = shift;

On récupère l'objet version du produit dans la base :

my $pv = $schema->resultset('Product')

-> find({name => $self->stash('name')})

-> product_versions

-> find ({version => $self->stash('version')}) ;

Ensuite, on crée les boutons radio actifs avec cette séquence un peu lourde (où $h est un objet HTML::Tiny). L'attribut check est calculé à la volée pour que le bouton affiche la valeur courante dans la base :

my @radio_items = map {

$h->label($_)

. $h->input({

type => "radio",

name => "v_status",

value => "$_",

($_ eq $pv->status) ? ( checked => 'checked') : ()

});

} qw/unstable testing stable/ ;

Enfin, on envoie le HTML pré-mâché au template :

$self->render (

template => 'mojo_template_mod',

radio_b => \@radio_items,

pv => $pv ,

) ;

};

Le template associé contient un formulaire. Pour se simplifier la vie, le bouton de sauvegarde va construire une URL avec l'id de la version plutôt qu'avec le couple « nom/version » :

@@mojo_template_mod.html.ep

% layout 'default' ;

% title 'Product status' ;

<h1>Product <%== $pv->product->name %> version <%== $pv->version %></h1>

<form name="save_data"

action="/product_version_data_save/<%= $pv->id %>"

method="post">

status :

<%== join("\n", @$radio_b) ; %>

<input type="submit"

name="save-product-version"

value="Save"/>

</form>

Et on obtient ce résultat, certes moche, mais fonctionnel :

Une fois le bouton « Save » pressé, le navigateur va envoyer une requête POST vers le serveur Mojolicious. Pour que cette requête puisse fonctionner, il faut la route associée.

Celle-ci va récupérer l'objet correspondant à la version modifiée et sauver le paramètre modifié dans la base.

post 'product_version_data_save/:vid' => sub {

my $self = shift ;

my $v_obj = $integ_schema

-> resultset('ProductVersion')

-> find({ id => $self->stash('vid')}) ;

# on extrait l'information des paramètres du POST

my $value = $self->param('v_status') ;

# on sauve dans l'objet version

$v_obj->status($value) ;

# enfin, on envoie la donnée dans la base

$v_obj->update ;

# le gros du boulot est fait, il reste à informer l'utilisateur

$self->render(

'product_version_save_done',

p => $v_obj->product,

v => $v_obj->version

);

} ;

Voici son template :

@@ product_version_save_done.html.ep

%title 'product '.$p->name . ' saved ' ;

%layout 'redirect', redirect_url => '/' ;

<p>Product <%= $p->name %> version <%== $v %> was saved</p>

<p><a href="/">go back to product list</a></p>

Au moins, ça fonctionne. Mais le code est déjà assez compliqué et les données ne sont pas vérifiées avant d'être envoyées dans la base (modify-a-la-mojo.pl dans [GITHUBADOD]).

Pour des formulaires plus compliqués que des boutons radio, il faut en plus s'assurer que les valeurs rentrées par l'utilisateur soient cohérentes. On peut toujours ajouter du code de validation dans les contrôleurs (c'est-à-dire dans les procédures associées aux routes), mais le mélange de la validation avec la génération des formulaires va compliquer la maintenance. L'idéal serait de pouvoir séparer la validation de la gestion des templates et des données.

Ben, justement, c'est ce que proposent les modules HTML::FormHandler et HTML::FormHandler::Model::DBIC.

4. Modification des données avec HTML::FormHandler

HTML::FormHandler est un module impressionnant qui permet de créer un formulaire HTML avec une déclaration de classe Perl en format Moose (ou presque). Cette classe est utilisée par HTML::FormHandler pour :

- Créer les formulaires HTML,

- Traiter et valider les paramètres envoyés par l'utilisateur avec une requête POST,

- Sauvegarder ces valeurs dans une base de données si HTML::FormHandler::Model::DBIC est aussi utilisé.

Je vous propose de faire un formulaire comprenant la fonction précédente (modification du statut) et l'ajout d'un log (pour montrer comment déclarer un champ avec une validation).

Tout d'abord, il faut créer une classe par formulaire. Cette classe doit utiliser HTML::FormHandler::Moose au lieu de Moose. Ça permet de déclarer les champs du formulaire avec has_field. Elle doit aussi hériter (déclaration avec extends) de HTML::FormHandler::Model::DBIC pour créer un formulaire dédié à une table de base de données. Comme cette classe va être incluse dans le fichier avec les routes de Mojolicious::Lite, on va l'englober entre deux accolades :

{

package ProductVersionForm;

use HTML::FormHandler::Moose;

extends 'HTML::FormHandler::Model::DBIC';

use namespace::autoclean;

Il faut ensuite déclarer quelle table va être utilisée pour sauvegarder les valeurs du formulaire. Il ne faut pas oublier le +, car cet attribut surcharge celui de la classe HTML::FormHandler.

has '+item_class' => ( default => 'ProductVersion' );

Ensuite, on peut déclarer les champs du formulaire.

status est un champ à plusieurs valeurs possibles (type select), où une seule valeur doit être choisie. Le plus simple est d'en faire un ensemble de boutons de type radio avec widget RadioGroup. Les informations passées avec options sont utilisées telles quelles dans la balise input. Chaque statut possible doit avoir une déclaration label et value pour que le champ soit affiché correctement.

has_field 'status' => (

type => 'Select',

widget => 'RadioGroup',

options => [

map { { label => $_ , value => $_ } ;} qw/unstable testing stable/

],

);

Maintenant, on va s'occuper du champ log. Pour emb^W inciter les utilisateurs à motiver le changement de statut, on rend le champ obligatoire avec required. Le paramètre apply permet de déclarer des contraintes de validation arbitraire (voire farfelues dans cet exemple). apply spécifie une condition à appliquer (une « subref » qui renvoie vrai ou faux) et un message d'erreur.

has_field 'log' => (

# utiliser TextArea pour une zone d'édition plus grande

type => 'Text',

required => 1,

apply => [

{

check => \&whatever,

message => 'whatever rule was not satisfied'

}

]

);

sub whatever {

my ( $value, $field ) = @_;

return $value =~ /whatever/ ? 1 : 0 ; # ok, c'est idiot

}

Enfin, le dernier champ spécifie l'indispensable bouton pour envoyer le formulaire :

has_field 'submit' => ( type => 'Submit', value => 'Save' );

Ces 2 dernières lignes sont des optimisations de performance :

__PACKAGE__->meta->make_immutable;

no HTML::FormHandler::Moose;

}

Maintenant, il faut revoir les contrôleurs. Les seuls qui changent sont ceux des routes /:name/#version et /product_version_data_save/:vid. Voici la première route qui va générer le formulaire :

get '/:name/#version' => sub {

my $self = shift;

my $product_version = $integ_schema

->resultset('Product')

->find( { name => $self->stash('name') } )

->product_versions

->find( { version => $self->stash('version') } );

my $form = ProductVersionForm->new(

action => "/product_version_data_save/".$product_version->id,

item => $product_version,

) ;

$self->render(

template => 'formhandler_template_mod',

my_form => $form ,

pv => $pv,

);

};

Il faut d'abord trouver dans la base l'objet version de produit (stocké dans $product_version) pour pouvoir afficher les valeurs courantes dans le formulaire. L'objet $form contient une instance de l'objet HTML::FormHandler. Le paramètre action spécifie où envoyer la requête POST. Le paramètre item doit contenir l'objet qui représente une ligne (« row ») dans la table ProductVersion. Enfin, l'appel à render utilise le template suivant :

@@formhandler_template_mod.html.ep

% layout 'default' ;

% title 'Product status' ;

<h1>Product <%== $pv->product->name %> version <%== $pv->version %></h1>

<%== $my_form->render%>

Celui-ci est très simple : il suffit d'appeler la méthode render sur l'objet formulaire pour créer tout le code HTML et produire cette page :

Et voici le contrôleur pour sauver les données :

post '/product_version_data_save/:vid' => sub {

my $self = shift;

my $form = ProductVersionForm->new;

# ne pas oublier de traiter les paramètres

$form->process(

schema => $integ_schema ,

item_id => $self->stash('vid') ,

params => $self->req->params->to_hash

) ;

if ($form->has_errors) {

return $self->render(text => join("\n", $form->errors )) ;

}

$self->render(

'product_version_save_done',

);

};

Il faut aussi recréer l'objet HTML::FormHandler formulaire (dans $form). L'appel à process contient le schéma et l'id de l'objet version de produit passé en paramètre du POST. HTML::FormHandler n'a pas besoin de plus pour savoir comment sauver les autres paramètres du POST contenus dans $self-req->params>. Le template appelé est très simple :

@@ product_version_save_done.html.ep

%title 'product saved ' ;

%layout 'redirect', redirect_url => '/' ;

<p>Product status was saved</p>

<p><a href="/">go back to product list</a></p>

Ce qui renvoie sur la page principale du serveur.

Ce web serveur est disponible dans modify-a-la-formhandler.pl sur le dépôt git [GITHUBADOD].

Conclusion

Nous voici arrivés à la fin de la trilogie. Vous avez le minimum pour pouvoir créer une petite application web avec une base de données relationnelle. N'hésitez pas à utiliser les codes sur le dépôt git [GITHUBADOD], ceux-ci sont disponibles en licence Artistic ou GPL-1+.

Dans votre projet, vous vous rendrez compte que ces articles ne montrent que le sommet de l'iceberg. Les modules DBIX::Class, Mojolicious et HTML::FormHandler ont beaucoup plus de possibilités. Il faudrait, non pas 3 articles pour les couvrir, mais 3 volumes. Heureusement, ces modules ont tous une documentation très complète sur CPAN. N'hésitez pas à la consulter !

Remerciements

Les Mongueurs de Perl pour leur accueil et la relecture de cet article.

Liens

[GLMF138] « Développement web en Perl avec Mojolicious » GNU/Linux Magazine n°138
[TIMTOWTDI] http://en.wikipedia.org/wiki/There%27s_more_than_one_way_to_do_it
[GITHUBADOD] https://github.com/dod38fr/glmf-article-dbix-class-web