Applications Web avec Catalyst

Daniel Brosseau

version 0.08

Résumé

Ce document présente l'utilisation de Catalyst, un framework orienté Web écrit en Perl


Table des matières

Introduction
Modèle MVC
Installation de Catalyst
Création d'une application
Ajout d'une vue
Exemples d'utilisation des TT
Cas du passage d'un tableau (au plutot d'une reference a un tableau)
Cas du passage d'un hachage (au plutot d'une reference a un hachage)
Résultat d'une requête SQL dans TT
Création d'un controleur
Création d'un Model
Catalyst::Enzyme pour plus de simplicité
Authentification
Création du Model 'Auth'
Création du controleur 'Login'
Ajout du template 'login.tt'
Ajout de 'Logout'
Intégration d'une barre de navigation
Authentification (Suite)
Prise en compte des roles
Creation de la page '/acces_refused'
Les roles dans le template
V0.0X
V0.02
V0.03
V0.04
Model et Mysql
Création de la base sous Mysql
Création d'une application accédant au Model CDBI
La methode 'search' de CDBI
Relation entre tables (relationship)
Has_a
Has_many
Insert, Delete et Cie
Insertion (find_or_create)
Mise à jour d'enregistrement (update)
Suppression d'enregistrement(s) (delete)
Mise en place d'un cache
Des modules Catalyst
Catalyst-Plugin-UploadProgress
Divers

Introduction

Perl est un language de programmation avec lequel je me débrouille tout juste alors pardonnez les erreurs qui apparaitront certainement au cours de l'écriture de ce document. N'hésitez pas à m'informer de toute remarque qui vous semblerai judicieuse.

Catalyst est un nouveau framework MVC en Perl destiné à faciliter considérablement la construction d'application Web. Il est aujourdhui en constante évolution.

Il utilise de nombreux modules Perl issu du CPAN ( search.cpan.org) et est au fait des dernières technos.

Modèle MVC

MVC signifie Model View Controler.

  • Les Models sont les définitions des espaces de stockages et elurs méthodes d'accès.

  • Les Views serviront dans la représentation des données.

  • Le rôle des controleurs est de gérer le flux de l'application.

Controller -> Prend les données dans Model

Controller -> Fourni les données à la View ou passe a un autre controleur, ...

Par exemple http://localhost:3000/Users/list est une action permettant de lister les utilisateurs de l'application. Dans le Controler 'Users' serai défini une méthode 'list' qui pourrai accéder aux données du Model 'Utilisateurs'. Les données retournées par les methodes du Model seraient stockées par le Controller qui les fournirai ensuite à la View.

Nous débuterons notre exploration à partir d'une installation vierge indépendante du système installé.

Installation de Catalyst

Prérequis:

  • Une Debian installée.

  • Accès au Net

Pour permettre l'utilisation de la dernière version de Catalyst nous allons créer une mini Debian qui contiendra tous les packages necessaires. Ainsi le système de base se sera pas affecté.

# 1 - Création du système Debian minimum
# Retrieve minimum packages for Debian Sid
[root@localtux]# debootstrap sid chroot_catalyst

# 2 - On prend pour racine ce nouveau système
[root@localtux]# chroot chroot_catalyst

# 3 - Montage du FS /proc
localhost:/# mount -t proc proc /proc


# 4 - Lecture des derniers packages
localhost:/# apt-get update

# 5 - Install minimum packages for catalyst
localhost:/# apt-get install locales gcc libc6-dev libperl-dev perl-modules dh-make-perl libdbi-perl libclass-dbi-perl libdbd-mysql-perl libclass-dbi-sqlite-perl libsql-abstract-limit-perl libclass-trigger-perl libdbix-contextualfetch-perl libhttp-server-simple-perl libwww-mechanize-perl libhttp-server-simple-perl libsqlite3-dev sqlite3 libtest-pod-perl libtest-pod-coverage-perl libclass-inspector-perl libperl6-slurp-perl subversion-tools libnet-ldap-perl shared-mime-info dnsutils libimage-imlib2-perl libgraphviz-perl librpc-xml-perl libtemplate-perl libgd-perl libclass-dbi-fromform-perl ftp mysql-client-4.1
localhost:/# dpkg-reconfigure locales 
localhost:/# apt-get clean (=~ 300 Mo)

# 6 - Init .cpan
localhost:/# perl -MCPAN -e shell

# 7 - Install lasted packages for Catalyst
localhost:/# perl -MCPAN -e shell << __EOF__
force install Test::WWW::Mechanize::Catalyst
install Task::Catalyst
install Catalyst::Enzyme
install Catalyst::View::GraphViz
install Catalyst::View::TT::ControllerLocal
install Catalyst::Plugin::Authorization::Roles
install Catalyst::Plugin::Authentication::Store::DBIC
install Catalyst::Helper::Controller::Scaffold
install Catalyst::Plugin::Authentication::CDBI
install DBI::Shell
__EOF__
       

Vous avez pu constater que Catalyst est dépendant d'un très grand nombre de modules perl.

Notre espace de travail étant à jour (=~ 330Mo), nous y resterons le reste du document.

Création d'une application

Nous crééons tout dabord le répertoire d'acceuil de notre application et nous nous y rendons.

localhost:/# mkdir -p /var/www/catalyst && cd /var/www/catalyst

Ensuite création de l'application

localhost:/var/www/catalyst/MonAppli/# catalyst.pl MonAppli
created "MonAppli"
created "MonAppli/script"
created "MonAppli/lib"
...

Et enfin exécution de l'application.

localhost:/var/www/catalyst/MonAppli/# cd MonAppli
localhost:/var/www/catalyst/MonAppli/# perl script/monappli_server.pl
[] [catalyst] [debug] Debug messages enabled
[] [catalyst] [debug] Loaded plugins:
.------------------------------------------------------------------------------.
| Catalyst::Plugin::Static::Simple                                             |
'------------------------------------------------------------------------------'

[] [catalyst] [debug] Loaded dispatcher "Catalyst::Dispatcher"
[] [catalyst] [debug] Loaded engine "Catalyst::Engine::HTTP"
[] [catalyst] [debug] Found home "/var/www/catalyst/MonAppli"
[] [catalyst] [debug] Loaded Private actions:
.----------------------+----------------------------------------+--------------.
| Private              | Class                                  | Method       |
+----------------------+----------------------------------------+--------------+
| /default             | MonAppli                               | default      |
'----------------------+----------------------------------------+--------------'

[] [catalyst] [info] MonAppli powered by Catalyst 5.61
You can connect to your server at http://localhost:3000

Et voilà notre première application est maintenant créée, elle est en écoute du port 3000. Pour le vérifier il suffit de se connecter à http://localhost:3000/

Le fonctionnement de l'application MonAppli est définie dans ./lib/MonAppli.pm. On constate que celle-ci hérite de '-Debug' 'Static::Simple'

use Catalyst qw/-Debug Static::Simple/;

-Debug nous permet d'accéder au mode Débug de Catalyst et Static::Simple gérera pour nous les pages statiques.

On y remarque le code suivant:

sub default : Private {
      my ( $self, $c ) = @_;

      # Hello World
      $c->response->body( $c->welcome_message );
}

Il s'agit du code qui sera utilisé si aucune autre action n'est exécutée. Pour plus de détails sur les actions prédéfinies (default, begin, end, auto, index) voir Wiki FlowChart

Au début on s'y pert un peu... Pour débugger on peut ajouter une ligne de code comme ceci:

$c->log->debug("[>> MonAppDeTest.pm sub default: bla bla bla ]") if $c->req->params->{debug};

Ajout d'une vue

Nous allons modifier l'application de manière a ce qu'elle utilise une vue héritant des templates toolkit. Voir Catalyst::View::TT et www.template-toolkit.org

Pour créer notre vue :

localhost:/var/www/catalyst/MonAppli/# perl script/monappli_create.pl view TT TT
 exists "/var/www/catalyst/MonAppli/script/../lib/MonAppli/View"
 exists "/var/www/catalyst/MonAppli/script/../t/View"
created "/var/www/catalyst/MonAppli/script/../lib/MonAppli/View/TT.pm"
created "/var/www/catalyst/MonAppli/script/../t/View/TT.t"

Nous pouvons maintenant utiliser cette vue.

Dans lib/MonAppli.pm nous modifions :

sub default : Private {
    my ( $self, $c ) = @_;

    # Hello World
    #$c->response->body( $c->welcome_message );
    $c->stash->{template}="mavue.tt";
    $c->stash->{mavariable}="MAVARIABLE";
}

sub end : Private {
    my ( $self, $c ) = @_;

    # Forward to View unless response body is already defined
    $c->forward('View::TT') unless $c->response->body;
}

Et nous créons le fichier root/mavue.tt

Ceci est le fichier mavue.tt
mavariable=[% mavariable %]

Nous redémarrons le serveur pour la prise en compte des modifications. Cette fois pour le redémmarrer nous utilsons l'option -r qui permet au serveur de redémarrer automatiquement si un fichier est modifié. Très utile lors du développement de l'application.

On indique à View::TT quel template utiliser en fournissant son nom a travers le stash. Pour plus d'infos sur le stash voir Manual Intro

Ensuite on transfert vers MonAppli::View::TT ($c->forward('MonAppli::View::TT');)

Et voilà on voit apparaitre "Ceci est le fichier mavue.tt" ainsi que [% mavariable %] subsititué.

Exemples d'utilisation des TT

Comme pour le template les variables seroint fournies a notre vue a travers le hachage $c->stash. Nous allons utiliser concretement les templates toolkit en leurs fournissants des donnes.

Cas du passage d'un tableau (au plutot d'une reference a un tableau)

Dans 'defaut' de lib/MonAppli.pm ajoutons:

$c->stash->{montableau} = [ 1, 3, 5, 7, 9 ]; # reference a un tableau

Et dans le template root/mavue.tt:

<br><b>Reference a un tableau</b><br>
montableau=[% montableau %]<br>
montableau[1]=[% montableau.1 %]<br>
montableau.first = [% montableau.first %]<br>
montableau.last = [% montableau.last %]<br>
montableau.size = [% montableau.size %]<br>

join(',',montableau) = [% montableau.join(', ') %]<br>


[% FOREACH donnee = montableau %]
        [% "Premier tour <br>" IF loop.first -%]	
	donnee=[% donnee %] loop.count=[% loop.count %]<br>
[% END %]

Cas du passage d'un hachage (au plutot d'une reference a un hachage)

Dans 'defaut' de lib/MonAppli.pm ajoutons:

$c->stash->{monhach} = {
		     couleur => 'rouge',
		     taille  => 'petit',
		     forme   => 'rond',
		     };	

Et dans le template root/mavue.tt:

<br><b>Reference a un hachage</b><br>
monhach=[% monhach %]<br>

monhach.couleur = [% monhach.couleur %]<br>

Infos sur les variables TT => Variables TT

Résultat d'une requête SQL dans TT

A faire ...

Création d'un controleur

Nous souhaitons accéder a notre vue avec une URL spécifique par exemple '/mavue'. Encore une fois (comme pour le vue TT) nous allons utiliser le 'Helper' de Catalyst pour créer le squelette de notre controleur.

localhost:/var/www/catalyst/MonAppli/# perl script/monappli_create.pl controller mavue

Ouvrons le fichier lib/MonAppli/Controller/mavue.pm. Comme pour l'application il y a encore une action 'default' commente cette fois ci. On la dcommente pour quelle devienne:

sub default : Private {
    my ( $self, $c ) = @_;
    $c->log->debug("[>> MonAppli/Controller/mavue.pm sub defaut ]") if $c->req->params->{debug};

    $c->stash->{mavariable}="MAVARIABLE";
    $c->stash->{montableau} = [ 1, 3, 5, 7, 9 ]; # reference a un tableau
    $c->stash->{monhach} = {
                     couleur => 'rouge',
                     taille  => 'petit',
                     forme   => 'rond',
                     };
    $c->stash->{template}="mavue.tt";
    $c->forward('MonAppli::View::TT');
}

Modifions a nouveau 'default' de lib/MonAppli.pm pour qu'il redevienne:

sub default : Private {
    my ( $self, $c ) = @_;

    # Hello World
    $c->response->body( $c->welcome_message );
}

Et voilà notre controler est prêt. http://localhost:3000/mavue

Donc jusqu'a maintenant nous avons:

  • Conserver la page par defaut (lib/MonAppli.pm sub default)

  • Ajouter le controleur 'mavue' qui 'forward' vers mavue.tt avec View::TT

Ces deux actions sont indiqués dans le log sous cette forme:

.----------------------+----------------------------------------+--------------.
| Private              | Class                                  | Method       |
+----------------------+----------------------------------------+--------------+
| /default             | MonAppli                               | default      |
| /mavue/default       | MonAppli::Controller::mavue            | default      |
'----------------------+----------------------------------------+--------------'

Nous pouvons ajouter des actions a notre controller.Dans notre exemple notre application devra retourne la date lorsque l'on accdera a /mavue/date.

Ajout a lib/MonAppli/Controller/mavue.pm de:

...
use Date::Calc qw(Localtime);
...
sub date : Global{
    my ( $self, $c ) = @_;

    my ($year,$month,$day, $hour,$min,$sec, $doy,$dow,$dst) = Localtime();
    my $date="Nous somme le $day/$month/$year il est $hour:$min:$sec";

    $c->stash->{date}=$date;
    $c->stash->{template}="date.tt";
    $c->forward('MonAppli::View::TT');
}

Et creation du template correspondant root/date.tt

Fichier date.tt<br>

[% date %]

Et voila on peut y accéder http://localhost:3000/date nous fourni la date. D'ailleurs on le vérifie dans le log:

.----------------------+----------------------------------------+--------------.
| Private              | Class                                  | Method       |
+----------------------+----------------------------------------+--------------+
| /default             | MonAppli                               | default      |
| /mavue/date          | MonAppli::Controller::mavue            | date         |
| /mavue/default       | MonAppli::Controller::mavue            | default      |
'----------------------+----------------------------------------+--------------'

[Mon Nov 21 13:47:15 2005] [catalyst] [debug] Loaded Path actions:
.--------------------------------------+---------------------------------------.
| Path                                 | Private                               |
+--------------------------------------+---------------------------------------+
| /date                                | /mavue/date                           |
'--------------------------------------+---------------------------------------

Pour rendre cette action accessible a partir de /mavue/date l'action ne doit plus etre Global mais Local. Pour les type d'actions possibles voir Manuel::Intro#Actions

[Mon Nov 21 14:34:51 2005] [catalyst] [debug] Loaded Path actions:
.--------------------------------------+---------------------------------------.
| Path                                 | Private                               |
+--------------------------------------+---------------------------------------+
| /mavue/date                          | /mavue/date                           |
'--------------------------------------+---------------------------------------'

Création d'un Model

Le Model défini les moyens d'accéder à une base de donnée. Dans cet exemple nous utiliserons une base sqlite.

Le schéma de notre base étant le suivant:

localhost:/var/www/catalyst/MonAppli/# cat ../exemple.sql
-- exempledb.sql
CREATE TABLE page (
   id_page INTEGER PRIMARY KEY,
   titre VARCHAR(40)
);

CREATE TABLE article(
   id_article INTEGER PRIMARY KEY,
   titre VARCHAR(40),
   contenu VARCHAR(2000)
);

CREATE TABLE page_article(
   id INTEGER PRIMARY KEY,
   page INTEGER REFERENCES page,
   article INTEGER REFERENCES article
);

INSERT INTO page VALUES(1, 'Titre de la premiere page');
INSERT INTO page values(2, 'Titre de la seconde page');

INSERT INTO article values(1, 'Titre Article 1', 'contenu article 1');
INSERT INTO article values(2, 'Titre Article 2', 'contenu article 2');
INSERT INTO article values(3, 'Titre Article 3', 'contenu article 3');
INSERT INTO article values(4, 'Titre Article 4', 'contenu article 4');

INSERT INTO page_article values(1,'1','1');
INSERT INTO page_article values(2,'1','2');
INSERT INTO page_article values(3,'2','3');
INSERT INTO page_article values(4,'2','4');

Pour créer la base sqlite, on utilise la commande suivante:

localhost:/var/www/catalyst/MonAppli/# sqlite3 exemple.db < ../exemple.sql

Crééons le Model qui utilisera cette base:

localhost:/var/www/catalyst/MonAppli/# perl script/monappli_create.pl model MonCDBI CDBI dbi:SQLite:/var/www/catalyst/MonAppli/exemple.db
 exists "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model"
 exists "/var/www/catalyst/MonAppli/script/../t"
created "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model/MonCDBI.pm"
created "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model/MonCDBI"
created "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model/MonCDBI/Article.pm"
created "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model/MonCDBI/Page.pm"
created "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model/MonCDBI/PageArticle.pm"
 exists "/var/www/catalyst/MonAppli/script/../t"
created "/var/www/catalyst/MonAppli/script/../t/model_MonCDBI-Article.t"
 exists "/var/www/catalyst/MonAppli/script/../t"
created "/var/www/catalyst/MonAppli/script/../t/model_MonCDBI-Page.t"
 exists "/var/www/catalyst/MonAppli/script/../t"
created "/var/www/catalyst/MonAppli/script/../t/model_MonCDBI-PageArticle.t"

Cool :) Notre Model 'MonCDBI' à découvert les tables de notre base. Mouai mais encore ...

Nous allons ensuite voir comment accéder aux données de nos tables.

Pour cela nous pouvons utiliser le controleur Scaffold de cette manière:

localhost:/var/www/catalyst/MonAppli/# perl script/monappli_create.pl controller Page Scaffold MonCDBI::Page
exists "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model"
created "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model/MonCDBI.pm"
created "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model/MonCDBI"
created "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model/MonCDBI/Article.pm"
created "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model/MonCDBI/Page.pm"
created "/var/www/catalyst/MonAppli/script/../lib/MonAppli/Model/MonCDBI/PageArticle.pm"
 exists "/var/www/catalyst/MonAppli/script/../t/Model"
created "/var/www/catalyst/MonAppli/script/../t/Model/MonCDBI-Article.t"
 exists "/var/www/catalyst/MonAppli/script/../t/Model"
created "/var/www/catalyst/MonAppli/script/../t/Model/MonCDBI-Page.t"
 exists "/var/www/catalyst/MonAppli/script/../t/Model"
created "/var/www/catalyst/MonAppli/script/../t/Model/MonCDBI-PageArticle.t"

Mais il existe depuis peu un module beaucoup plus simple d'utilisation et qui va nous faciliter grandement la tâche :)

Catalyst::Enzyme pour plus de simplicité

Catalyst::Enzyme nous facilite encore plus la tâche :)

Testons le immédiatement. Pour cela nous réutiliserons le schema de la base SQLite exemple.sql

Lors de la création automatique de vue,controleur ou model nous avons utilisé des 'Helpers'. Ils sont fournis pour nous aider à construire le squelette de scripts. Catalyst::Enzyme fourni aussi ses propres Helpers

Il va nous aider dans la création des scripts d'accès aux données d'une table.

localhost:/var/www/catalyst/# catalyst.pl TestEnzyme && cd TestEnzyme
localhost:/var/www/catalyst/TestEnzyme/# vi lib/TestEnzyme.pm

On remplace
use Catalyst qw/-Debug Static::Simple/;

par
use Catalyst qw/-Debug Static::Simple DefaultEnd FormValidator/;

et
$c->response->body( $c->welcome_message );

par
$c->res->redirect("/page");

localhost:/var/www/catalyst/TestEnzyme/# perl script/testenzyme_create.pl view TT Enzyme::TT
 exists "/var/www/catalyst/TestEnzyme/script/../lib/TestEnzyme/View"
 exists "/var/www/catalyst/TestEnzyme/script/../t"
created "/var/www/catalyst/TestEnzyme/script/../lib/TestEnzyme/View/TT.pm"
created "/var/www/catalyst/TestEnzyme/script/../root/base/add.tt"
created "/var/www/catalyst/TestEnzyme/script/../root/base/edit.tt"
created "/var/www/catalyst/TestEnzyme/script/../root/base/footer.tt"
created "/var/www/catalyst/TestEnzyme/script/../root/base/form_macros.tt"
created "/var/www/catalyst/TestEnzyme/script/../root/base/header.tt"
created "/var/www/catalyst/TestEnzyme/script/../root/base/list.tt"
created "/var/www/catalyst/TestEnzyme/script/../root/base/list_macros.tt"
created "/var/www/catalyst/TestEnzyme/script/../root/base/pager.tt"
created "/var/www/catalyst/TestEnzyme/script/../root/base/pager_macros.tt"
created "/var/www/catalyst/TestEnzyme/script/../root/base/view.tt"
created "/var/www/catalyst/TestEnzyme/script/../root/static/css/testenzyme.css"
created "/var/www/catalyst/TestEnzyme/script/../t/view_TT.t"


localhost:/var/www/catalyst/TestEnzyme/# mkdir db && dbish dbi:SQLite:dbname=db/exemple.db < ../exemple.sql
localhost:/var/www/catalyst/TestEnzyme/# perl script/testenzyme_create.pl model ExempleDB Enzyme::CDBI dbi:SQLite:dbname=db/exemple.db
localhost:/var/www/catalyst/TestEnzyme/# perl script/testenzyme_create.pl controller Page Enzyme::CRUD ExempleDB::Page
localhost:/var/www/catalyst/TestEnzyme/# perl script/testenzyme_create.pl controller Article Enzyme::CRUD ExempleDB::Article

Cool :)

Avec une page de style, un pager ... ça a une autre gueule non ?

En fait pour profiter du 'pager' (permet d'afficher qu'un nombre limité de données ) il nous faut tout dabord modifier notre Modele Page lib/TestEnzyme/Model/ExempleDB/Page.pm.

Remplacer
__PACKAGE__->config(
    crud => {
        
    }
);

par
__PACKAGE__->config(

        crud => {
            moniker => "Page",
            column_monikers => { __PACKAGE__->default_column_monikers, titre => "Le titre" },
            rows_per_page => 5,
            data_form_validator => {
                optional => [ __PACKAGE__->columns ],
                required => [ qw/ titre /],
                constraint_methods => {
                    titre => { name => 'contraintetitre', constraint => qr/^[A-Z]/ },
                },
                missing_optional_valid => 0,
                msgs => {
                    format => '%s',
                    constraints => {
                        contraintetitre => "Le titre doit d~buter par une majuscule !",
                    },
                },
            },
        },
);

  • column_monikers : permet de renommer une colonne a l'affichage

  • rows_per_page: comme son nom l'indique, nombre de rangée de données par page (utilisé par le 'pager')

  • data_form_validator: Nous permet de valider les donnée fourni par l'utilisateur. Dans notre exemple si aucune donnée n'est fournie au champ 'titre' (required) alors les données ne sont pas validées et donc non enregistrées dans par l'action 'do_add'.

  • constraint_methods: Contrainte sur les données. Notre titre doit commencer par une majuscule

Il nous est aussi possible de modifier les champs/colonnes à afficher. Pour notre Model 'Article' remarquez que seul les champs 'contenu' et 'titre' sont affichés, 'id_article' ne l'est pas. Pour forcer son affichage lors du listing '/list' nous modifions la ligne suivante du Model 'Article' (lib/TestEnzyme/Model/ExempleDB/Article.pm):

__PACKAGE__->columns(list_columns => qw/ contenu titre /);

par
__PACKAGE__->columns(list_columns => qw/ id_article contenu titre /);

Il est aussi possible de modifier les champs à afficher lors de l'ajout et de la vue d'article.

__PACKAGE__->columns(view_columns => qw/ contenu titre /);

Authentification

Nous n'allons pas laisser n'importe qui accéder en écriture à notre base de données, il nous faut donc un mécanisme d'authentification. Encore une fois Catalyst est là pour nous aider :)

Pour assurer l'authentication des utilisateurs nous utiliserons le module Catalyst::Plugin::Authentication::CDBI. Celui-ci ne se contente pas seulement de gérer les utilisateurs mais aussi le 'role' des utilisateurs. Nous déciderons par exemple que l'utilisateur 'toto' a le 'role' admin.

Les utilisateurs ainsi que les roles seront stockés en base. Une fois encore nous utiliserons SQLite.

localhost:/var/www/catalyst/TestEnzyme/# cat ../auth.sql
-- Users
CREATE TABLE user (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(30) NOT NULL,
        password VARCHAR(40) NOT NULL,
        firstname VARCHAR(40),
        lastname VARCHAR(40)
);

-- Roles
CREATE TABLE role (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(30)
);

-- Mapping
CREATE TABLE user_role (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        user INTEGER REFERENCES user,
        role INTEGER REFERENCES role
);

-- Users (pass: 12345)
REPLACE INTO user VALUES (1, 'admin', '12345','Robert','Dupont');
REPLACE INTO user VALUES (2, 'user', '12345','gaston','lagaffe');
REPLACE INTO user VALUES (3, 'toto', '12345','gaston','lagaffe');

-- Roles
REPLACE INTO role VALUES (1, 'admin');
REPLACE INTO role VALUES (2, 'writer');
REPLACE INTO role VALUES (3, 'reader');

-- User Roles
REPLACE INTO user_role VALUES (1, 1, 1);
REPLACE INTO user_role VALUES (2, 1, 2);
REPLACE INTO user_role VALUES (3, 1, 3);
REPLACE INTO user_role VALUES (4, 2, 2);
REPLACE INTO user_role VALUES (5, 2, 3);
REPLACE INTO user_role VALUES (6, 3, 3);

localhost:/var/www/catalyst/TestEnzyme/# dbish dbi:SQLite:dbname=db/auth.db < ../auth.sql

Notre application doit tout dabord hériter du module 'Catalyst::Plugin::Authentication::CDBI' et de 'Session::FastMmap'. Ajoutons les simplement dans le fichier lib/TestEnzyme.pm.

use Catalyst qw/-Debug Static::Simple DefaultEnd FormValidator Session::FastMmap Authentication::CDBI/;

...

# Authentication
__PACKAGE__->config->{authentication} = {
        user_class           => 'TestEnzyme::Model::Auth::User',
        user_field           => 'username',
        role_class           => 'TestEnzyme::Model::Auth::Role',
        role_field           => 'role',
        user_role_class      => 'TestEnzyme::Model::Auth::UserRole',
        user_role_user_field => 'user',
        user_role_role_field => 'role'
    };

...

sub begin : Private {
    my ( $self, $c ) = @_;
 
    $c->res->headers->content_type( 'text/html; charset=iso-8859-1' );

    my $result=$c->session_login('admin', '8cb2237d0679ca88db6464eac60da96345513964');
    $c->log->debug("result=$result");
}

...
sub end : Private {
    my ( $self, $c ) = @_;

    die "Debug forc~" if $c->req->params->{die};
}

'begin' est la première action exécutée, nous vérifions simplement que le login fonctionne. Dans le log du serveur devrait apparaitre '[catalyst] [debug] result=1'. ( Créer tout dabord le Model 'Auth' ci-dessous). $c->res->headers->content_type nous permet la prise en compte des accents en français.

'end' est la dernière 'action' exécutée. Elle nous permettra le debuguage des scripts, il suffira alors ajouter '?die=1' à l'url pour accéder à la page de debug.

Création du Model 'Auth'

Notre module d'authentification fait appel au Model 'Auth' qui n'a pas encore été créé. Ce que nous faisons tout de suite.

localhost:/var/www/catalyst/TestEnzyme/# perl script/testenzyme_create.pl model Auth CDBI dbi:SQLite:dbname=db/auth.db
 exists "/var/www/catalyst/TestEnzyme/script/../lib/TestEnzyme/Model"
 exists "/var/www/catalyst/TestEnzyme/script/../t"
created "/var/www/catalyst/TestEnzyme/script/../lib/TestEnzyme/Model/Auth.pm"
created "/var/www/catalyst/TestEnzyme/script/../lib/TestEnzyme/Model/Auth"
created "/var/www/catalyst/TestEnzyme/script/../lib/TestEnzyme/Model/Auth/Role.pm"
created "/var/www/catalyst/TestEnzyme/script/../lib/TestEnzyme/Model/Auth/User.pm"
created "/var/www/catalyst/TestEnzyme/script/../lib/TestEnzyme/Model/Auth/UserRole.pm"
 exists "/var/www/catalyst/TestEnzyme/script/../t"
created "/var/www/catalyst/TestEnzyme/script/../t/model_Auth-Role.t"
 exists "/var/www/catalyst/TestEnzyme/script/../t"
created "/var/www/catalyst/TestEnzyme/script/../t/model_Auth-User.t"
 exists "/var/www/catalyst/TestEnzyme/script/../t"
created "/var/www/catalyst/TestEnzyme/script/../t/model_Auth-UserRole.t"

En redémarrant le serveur nous pouvons vérifier que les tables sont correctement chargées et que result est bien égal à '1' ce qui nous confirmera qur l'authentification s'est correctement déroulée. Un cookie à aussi été créé contenant le numéro de session.

[] [catalyst] [debug] Loaded tables "article page page_article"
[] [catalyst] [debug] Loaded tables "role user user_role"
...
[] [catalyst] [debug] result=1

Nous allons dans un premier temps imposer une authentification des utilisateurs quelle que soit la page accédée. Pour celà nous modifions l'action 'begin' du fichier principal de notre application lib/TestEnzyme.pm

sub begin : Private {
    my ( $self, $c ) = @_;

    # Pour les accents
    $c->res->headers->content_type( 'text/html; charset=iso-8859-1' );

    # force login for all pages
    unless ($c->req->{user}) {
      $c->req->action(undef);
      $c->forward('/login/login');
    }
}

'begin' étant éxécuté avant tout autre action, cela oblige les utilisateurs à se loguer.

Nous constatons que l'action 'begin' n'est pas exécutée. Hmmm ... Pourquoi ???. Rappelons nous que nous venons de tester le fonctionnement du 'login' et qu'un cookie a été créé. Supprimons le et là et bien ...

Création du controleur 'Login'

Il nous faut donc créer le controleur 'Login'.

localhost:/var/www/catalyst/TestEnzyme/# perl script/testenzyme_create.pl controller Login

Et l'action login du controleur 'Login' (lib/TestEnzyme/Controller/Login.pm

sub default : Private {
  my ($self, $c) = @_;
  $c->forward('login');
}

sub login : Path('/login') {
  my ( $self, $c ) = @_;

  if ($c->req->params->{username}) {
      my $login=$c->session_login(
                      $c->req->params->{username},
                      $c->req->params->{password}
                     );
     if ( $login ){
       $c->res->redirect( $c->session->{referer} || '/' );
     }
     else{
       $c->stash->{template} = "login.tt"
     }
  }
  else {
    # save the referring page so we can redirect back
    $c->session->{referer} = $c->req->path;
    $c->stash->{template} = "login.tt"
  }
}

Si nous ne sommes pas authentifié alors nous sommes redirigé vers la page de login. Le reste se passe de commentaire :)

Ajout du template 'login.tt'

Il nous faut encore créer le template 'login.tt'

localhost:/var/www/catalyst/TestEnzyme/# cat root/login.tt
[% INCLUDE base/header.tt %]

<form action="/login" method="post">
<fieldset>
        <legend>Connexion.</legend>
        <label for="username"><span class="field">Login:</span></label>
        <input type="text" name="username" /><br />

        <label for="password"><span class="field">Password:</span></label>
        <input type="password" name="password" /><br />

        <label for="submit"><span class="field"></span></label>
        <input type="submit" value="Se connecter" /><br />
</fieldset>
</form>


[% IF ! c.req.user %]
<script language="javascript">
        document.forms[0].username.focus();
</script>
[% END %]

<fieldset>
        Se connecter en admin password=12345 ou user/12345 ou toto/12345
</fieldset>

Terminé. Notre système d'authentification est en place :)

Nous allons modifier maintenant modifier un peu l'authentification. L'utilisateur pourra voir divers éléments selon son 'role'. S'il n'est pas logué alors il ne dispose d'aucun 'role' et un onglet 'login' apparait en haut de page. S'il est logué un onglet 'logout' apparait en haut de page. Pour y parvenir nous devrons:

  • Créer une action 'Logout'.

  • Supprimer l'action 'begin' de l'application qui redirige s'il l'utilisateur n'est pas logué

  • Modifier nos templates pour qu'ils fasse nt apparaitre une barre de navigation (Login/Logout)

Ajout de 'Logout'

L'ajout de l'action 'Logout' se fait simplement en modifiant/ajoutant comme suit le code de lib/TestEnzyme/Controller/Login.pm

sub login : Path('/login') {
  my ( $self, $c ) = @_;

  if ($c->req->params->{username}) {
      my $login=$c->session_login(
                      $c->req->params->{username},
                      $c->req->params->{password}
                     );
     if ( $login ){
          $c->res->redirect( '/' );
     }
     else{
       $c->stash->{template} = "login.tt"
     }
  }
  else {
    # save the referring page so we can redirect back
    $c->session->{referer} = $c->req->path;
    $c->stash->{template} = "login.tt"
  }
}

sub logout : Path('/logout') {
        my ($self, $c) = @_;

        $c->session_logout if ($c->req->{user});
        $c->res->redirect('/login/login');
}

On peut dès à présent utiliser l'url 'login/logout' qui après avoir supprimer la session nous redirige vers 'login/login'.

TODO: Crypter les mots de passe dans db/auth.db

Intégration d'une barre de navigation

Pour intégrer notre 'barre de navigation' à nos page nous allons l'ajouter à la manière de ce qui est fait dans les templates qui ont été créés automatiquement (root/base/[add,list,edit,view].tt). Ont peut constater qu'il y a un '[% INCLUDE 'header.tt' %]' dans chacun de ceux-ci. C'est donc dans ce template que nous ferons notre modification.

localhost:/var/www/catalyst/TestEnzyme/# cat root/base/header.tt
...
[% INCLUDE "navbar.tt" %]


Le barre de navigation root/base/navbar.tt se présente sous cette forme

localhost:/var/www/catalyst/TestEnzyme/# cat root/base/navbar.tt
<div id="navcontainer">
        <ul class="navlist">
                <li [% IF nav == 'acceuil' %]id="active"[% END %]><a href="/">Acceuil</a></li>
                <li [% IF nav == 'search' %]id="active"[% END %]><a href="/page">Page</a></li>
                <li [% IF nav == 'report' %]id="active"[% END %]><a href="/article">Article</a></li>

                [% IF ! c.req.user %]<li><a href="/login/login">Login</a></li>[% END %]
                [% IF c.req.user %]<li><a href="/login/logout">Logout</a></li>[% END %]
        </ul>
</div>

On remarque que le paramètre 'nav' permet d'utiliser un 'id active' qui pourra être utilisée par la feuille de style CSS. Nous y reviendrons plus tard. Voyons ce que ça donne ...

Oui il fallait s'y attendre, notre barre de navigation va devoir être 'habillée' par la page CSS root/static/css/testenzyme.css. On y ajoute ce qui suit

.navlist {
    padding: 3px 0;
    margin-left: 0;
    margin-top: 1em;
    border-bottom: 1px solid #778;
#    font: bold 12px Verdana, sans-serif;
}

.navlist li {
    list-style: none;
    margin: 0;
    display: inline;
}

.navlist li a {
    padding: 3px 0.5em;
    margin-left: 3px;
    border: 1px solid #778;
    border-bottom: none;
    background: #b5cadc;
    text-decoration: none;
}

.navlist li a:link { color: #448; }
.navlist li a:visited { color: #667; }

.navlist li a:hover {
    color: #000;
    background: #eef;
    border-top: 4px solid #7d95b5;
    border-color: #227;
}

.navlist #active a {
    background: white;
    border-bottom: 1px solid white;
    border-top: 4px solid;
}

.loginas{
  background-color:#FF0000;
 }
 

Ah oui là c'est mieux :)

Le paramètre 'nav' va permettre le colorier l'onglet relatif à la page à laquelle on accède. Pour cela modifons l'action 'default' de lib/TestEnzyme.pm pour qu'il nous transfert vers un template 'index.tt' et l'action 'end' qui enregistre dans le stash nav='acceuil'.

sub default : Private {
    my ( $self, $c ) = @_;


    # Hello World
    $c->stash->{template} ||= "index.tt";
}

sub end : Private {
    my ( $self, $c ) = @_;

    $c->stash->{nav} = "acceuil" unless $c->stash->{nav};

    # Forward to View unless response body is already defined
    $c->forward('View::TT') unless $c->response->body;
        die "Debug forc~" if $c->req->params->{die};
}

Au tours du template root/base/index.tt

[% INCLUDE "header.tt" %]
[% INCLUDE "navbar.tt" %]

Page d'acceuil.  ( ~ ~ ~ ~ ~ )

[% INCLUDE "footer.tt" %]

L'onglet de la page par defaut 'acceuil' est maintenant mise en évidence.

Nous allons procéder de manière identique pour les autres onglets. Les onglets nous transfert vers les controleurs 'Page' et 'Article', c'est donc dans ces controleurs que nous fixerons le paramètre 'nav' spécifique et plus extactement dans l'action 'begin' du controleur. Pour le controleur 'Page' () nous aurons donc

sub begin : Private {
    my ( $self, $c ) = @_;

    $c->stash->{nav} = "page";
}

Et nous ferons de même pour le controleur 'Article'. Notre barre de navigation fonctionne, elle est facilement maintenable et il est possible d'en ajouter ou supprimer facilemnt des onglets. Nous pourrions sur le même principe mettre en place une barre de nagation de second niveau en fonction de l'onglet sur lequel on est situé.

On continue ... Nous aimerions pouvoir accéder au site ou du moins à certaines rubriques sans pour autant être logué. Supprimons l'action 'begin' de lib/TestEnzyme.pm. Il n'est plus maintenant necessaire de se loguer mais n'importe qui accède à notre base exemple.db. Il nous faut donc aussi modifié les templates pour qu'ils prennent en compte le 'role' des utilisateurs.

Nota: Les actions 'delete' et 'view' ne fonctionne qu'a partir de la version 0.09 de Catalyst::Enzyme.

Authentification (Suite)

Prise en compte des roles

Le plugin Catalyst Authentification::CDBI prend en compte les roles, mais comment les utiliser. Nous pourrions par exemple ajouter ce code a notre controleur 'Page'.

sub begin : Private {
    my ( $self, $c ) = @_;

    if ( ! $c->roles('admin') ){
            $c->res->redirect('/acces_refused');
    }

    $c->stash->{nav} = "page";
}

Si le role n'est pas 'admin' et que l'on accède à '/page' nous sommes redirigé vers '/acces_refused'. Nous ferons de même pour le controleur 'Article' et enfin nous ajouterons l'action 'acces_refused' à notre application qui fourniera un temmplate 'acces_refused.tt'.

Creation de la page '/acces_refused'

Dans lib/TestEnzyme.pm nous ajoutons l'action 'acces_refused':

sub acces_refused : Private {
    my ( $self, $c ) = @_;

    $c->stash->{template} = "acces_refused.tt";
}

Et nous ajoutons le template root/base/acces_refused.tt

[% INCLUDE "header.tt" %]
[% INCLUDE "navbar.tt" %]

Accès refusé

[% INCLUDE "footer.tt" %]

Les roles dans le template

De plus il ne faut pas que les onglets 'Page' et 'Article' apparaissent si nous n'avons pas le role 'admin'. 'navbar.tt' nous permet l'affichage de la barre de navigation c'est donc ce fichier (root/base/navbar.tt)que nous allons modifier comme ceci:

                [% IF c.roles('admin') %]
                <li [% IF nav == 'page' %]id="active"[% END %]><a href="/page">Page</a></li>
                <li [% IF nav == 'article' %]id="active"[% END %]><a href="/article">Article</a></li>
                [% END %]

V0.0X

V0.02

Voici la version 0.02 de notre application : TestEnzyme-0.02.tgz. Elle contient le code de l'application que nous venons de construire.

V0.03

Les modifications suivantes ont été apportée à la version 0.03 de notre application 'TestEnzyme': TestEnzyme-0.03.tgz

  • Ajout controleur 'Preferences'

  • Ajout onglet 'nom de l'utilisateur' qui pointe vers le controleur '/preference'

  • 'error' est utilisé dans header.tt pour donner la raison lors des accès a '/acces_refused'.

  • Nettoyage du code (suppression des '^M' de notepad ? :)

V0.04

Les modifications suivantes ont été apportée à la version 0.04 de notre application 'TestEnzyme': TestEnzyme-0.04.tgz

  • Prise en compte de la version 0.09 de Catalyst::Enzyme ('view' et 'delete' fonctionne correctement)

  • 'navbar.tt' déplacé dans 'header.tt'

Model et Mysql

Dans ce chapitre nous abandonnerons notre précédente application pour nous focaliser sur le model CDBI. (voir Class::DBI)

Création de la base sous Mysql

Notre Model utilisera une base Mysql. Pour ne pas trop se compliquer la vie, la base ne sera pas installée dans notre 'chroot'. Ce pourra être une base installée sur une autre machine ou en local mais en dehors du 'chroot'. Notre schéma sera similaire a celui d'exemple.sql à l'exception d'un champ 'date_creation' (que nous utiliserons plus tard) ajouté à la table 'article' et de la mise en place de droit d'accès à celle-ci. Nous ajouterons aussi une table 'auteur'. On sort du chroot (Control D)

tux:# cat exemple2.sql
-- exemple2db.sql
DROP DATABASE IF EXISTS exemple2;
CREATE DATABASE exemple2;

use exemple2;

CREATE TABLE page (
   id_page INTEGER PRIMARY KEY,
   titre VARCHAR(40),
   auteur INTEGER REFERENCES auteur
);

CREATE TABLE article(
   id_article INTEGER PRIMARY KEY,
   page INTEGER REFERENCES page,
   titre VARCHAR(40),
   date_creation DATE,
   contenu VARCHAR(2000)
);

CREATE TABLE auteur (
   id_auteur INTEGER PRIMARY KEY,
   nom VARCHAR(40)
);


INSERT INTO page VALUES(1, 'Titre de la premiere page', 1);
INSERT INTO page values(2, 'Titre de la seconde page', 2);

INSERT INTO article values(1, 1, 'Titre Article 1', '2005-12-10','contenu article 1');
INSERT INTO article values(2, 1, 'Titre Article 2', '2005-12-10','contenu article 2');
INSERT INTO article values(3, 2, 'Titre Article 3', '2005-12-10','contenu article 3');
INSERT INTO article values(4, 2, 'Titre Article 4', '2005-08-01','contenu article 4');

INSERT INTO auteur values(1, 'dab');
INSERT INTO auteur values(2, 'newton');

GRANT ALL PRIVILEGES ON exemple2.* to myuser@localhost IDENTIFIED BY 'passwd';

Création de la base 'exemple2':

:# mysql -u root -p < exemple2.sql
Enter password:

Ok notre base est créée. Vérifions que nous pouvons y accéder. On retourne dans notre chroot

tux:# chroot chroot_catalyst
localhost:# mysql -u myuser -p -h 127.0.0.1  exemple2
Enter password:(passwd)
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 26 to server version: 4.1.15-Debian_1-log

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> show tables;
+--------------------+
| Tables_in_exemple2 |
+--------------------+
| article            |
| auteur             |
| page               |
+--------------------+
3 rows in set (0.00 sec)

OK notre accès à 'exemple2' par le réseau est satisfaisant.

Création d'une application accédant au Model CDBI

C'est reparti ... Heu ... pas tout à fait, il nous faut tout dabord installé le module Perl Class::DBI::mysql. Malheureusement celui-ci tombe en erreur lorsque nous tentons de l'installer par la méthode 'perl -MCPAN -e "install Class::DBI::mysq" car il essaye d'accéder en local à la base de test de Mysql ( failed: Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock'). Nous l'installerons donc sans procéder aux tests:

localhost:# cd /root/.cpan/build/Class-DBI-mysql-1.00 && perl Makefile.PL && make && make install && cd /var/www/catalyst
localhost:/var/www/catalyst# catalyst.pl AppCDBI && cd AppCDBI
localhost:/var/www/catalyst/AppCDBI# perl script/appcdbi_create.pl model Exemple2DB CDBI 'dbi:mysql:database=exemple2;host=127.0.0.1' 'myuser' 'passwd'
 exists "/var/www/catalyst/AppCDBI/script/../lib/AppCDBI/Model"
 exists "/var/www/catalyst/AppCDBI/script/../t"
 exists "/var/www/catalyst/AppCDBI/script/../lib/AppCDBI/Model/Exemple2DB.pm"
created "/var/www/catalyst/AppCDBI/script/../lib/AppCDBI/Model/Exemple2DB.pm.new"
created "/var/www/catalyst/AppCDBI/script/../lib/AppCDBI/Model/Exemple2DB"
created "/var/www/catalyst/AppCDBI/script/../lib/AppCDBI/Model/Exemple2DB/Article.pm"
created "/var/www/catalyst/AppCDBI/script/../lib/AppCDBI/Model/Exemple2DB/Auteur.pm"
created "/var/www/catalyst/AppCDBI/script/../lib/AppCDBI/Model/Exemple2DB/Page.pm"
 exists "/var/www/catalyst/AppCDBI/script/../t"
created "/var/www/catalyst/AppCDBI/script/../t/model_Exemple2DB-Article.t"
 exists "/var/www/catalyst/AppCDBI/script/../t"
created "/var/www/catalyst/AppCDBI/script/../t/model_Exemple2DB-Auteur.t"
 exists "/var/www/catalyst/AppCDBI/script/../t"
created "/var/www/catalyst/AppCDBI/script/../t/model_Exemple2DB-Page.t"

Pour faire nos divers tests nous créons une vue et son template (root/test.tt) et comme précédement modifions l'action 'end' de notre application (lib/AppCDBI.pm) pour quelle 'forward' vers notre vue. :

localhost:/var/www/catalyst/AppCDBI# perl script/appcdbi_create.pl view TT TT
localhost:/var/www/catalyst/AppCDBI# cat lib/AppCDBI.pm
...

...
sub end : Private {
    my ( $self, $c ) = @_;

    $c->forward('View::TT'); 
}

localhost:/var/www/catalyst/AppCDBI# cat root/test.tt

Test Class::DBI<br>

colonnes page: [% colonnes.join(', ') %]<br>
pages: [% pages.join(', ') %]<br>

Et enfin le controleur associé '/test'

localhost:/var/www/catalyst/AppCDBI# perl script/appcdbi_create.pl controller test

C'est dans ce controleur que nous effecterons nos divers requetes SQL dont le résultat sera stocké dans le 'stash' ($c->stash->{result}).

localhost:/var/www/catalyst/AppCDBI# cat lib/AppCDBI/Controller/test.pm
...

sub default : Local {
    my ( $self, $c ) = @_;

    $c->stash->{template} = "test.tt";
    my @colonnes_pages= AppCDBI::Model::Exemple2DB::Page->columns;
    my @pages= AppCDBI::Model::Exemple2DB::Page->retrieve_all;

    $c->stash->{colonnes} = \@colonnes_pages;
    $c->stash->{pages} = \@pages;

}
...

http://localhost:3000/test nous retourne

test Class::DBI
colonnes page: id_page, auteur, titre
pages: 1, 2

On constate que le 'retrieve_all' retourne l'id_page des pages. En fait 'retrieve_all' retourne un 'iterator' que nous pouvons utiliser comme ceci dans le template root/test.tt:

[% FOREACH p = pages %]
      Id: [% p.id_page %] Titre: [% p.titre %] Auteur: [% p.auteur %]<br>
[% END %]

Ce qui nous donne:

 Id: 1 Titre: Titre de la premiere page Auteur: 1
Id: 2 Titre: Titre de la seconde page Auteur: 2

La methode 'search' de CDBI

Si nous souhaitons faire une recherche en base nous utiliserons la methode 'search' de notre Modèle CDBI. Par exemple nous cherchons tous les articles publié le 10/12/2005. Modifions le controleur 'test' comme ci-dessous:

    my @articles = AppCDBI::Model::Exemple2DB::Article->search( date_creation => '2005-12-10');
    $c->stash->{articles} = \@articles;

Et ajoutons au template 'test.tt' ceci

[% FOREACH article = articles %]
      Id: [% article.id_article %] Titre: [% article.titre %]<br>
[% END %]

Qui nous retourne :

Id: 1 Titre: Titre Article 1
Id: 2 Titre: Titre Article 2
Id: 3 Titre: Titre Article 3

Il nous est bien sur possible de trier les données par un 'order_by'. Ainsi dans l'exemple ci-dessus nous pourrions vouloir trier les données selon leur date de création :

    my @articles = AppCDBI::Model::Exemple2DB::Article->search_like( titre => 'Titre Article%', { order_by => 'date_creation'});

Qui nous retourne :

Id: 4 Titre: Titre Article 4
Id: 1 Titre: Titre Article 1
Id: 2 Titre: Titre Article 2
Id: 3 Titre: Titre Article 3

Relation entre tables (relationship)

Jusque là nous avons effectué des requêtes sur des tables uniques, mais comment procéder pour faire référence à plusieurs tables. Comment par exemple selectionner "toutes les pages dont l'auteur est 'dab' ou encore "tous les articles de la page 1"? C'est là qu'interviennent les 'relationship'.

Has_a

Dans un premier temps nous allons afficher le nom de l'auteur et non plus son 'id', pour cela nous spécifions dans le Modele Page lib/AppCDBI/Model/Exemple2DB/Page.pm la relation qui existe entre le champ 'auteur' de la table Page et la table Auteur. Pour chaque page il n'existe qu'un auteur.

__PACKAGE__->has_a( auteur => 'AppCDBI::Model::Exemple2DB::Auteur');

et dans le template 'test.tt nous remplacons :

Id: [% p.id_page %] Titre: [% p.titre %] Auteur: [% p.auteur %]

par 

Id: [% p.id_page %] Titre: [% p.titre %] Auteur: [% p.auteur.nom %]

Ok cette fois ci nous avons bien le nom en clair de l'auteur:

 Id: 1 Titre: Titre de la premiere page Auteur: dab
Id: 2 Titre: Titre de la seconde page Auteur: newton

Has_many

Pour chaque page il existe plusieurs articles, on peut donc écrire dans lib/AppCDBI/Model/Exemple2DB/Page.pm

__PACKAGE__->has_many( article => 'AppCDBI::Model::Exemple2DB::Article');

Et on modifie le template 'test.tt' comme ceci:

[% FOREACH p = pages %]
      Id: [% p.id_page %] Titre: [% p.titre %] Auteur: [% p.auteur.nom %]<br>
      [% FOREACH article = p.article %]
         Article: [% article %] -> [% article.titre %]<br>
      [% END %]
[% END %]

Et là pour chacune des pages nous avons bien les articles correspondants:

Id: 1 Titre: Titre de la premiere page Auteur: dab
Article: 1 -> Titre Article 1
Article: 2 -> Titre Article 2
Id: 2 Titre: Titre de la seconde page Auteur: newton
Article: 3 -> Titre Article 3
Article: 4 -> Titre Article 4

Yep :)

Et le source de cette l'application AppCDBI.tgz

Insert, Delete et Cie

Jusqu'a maintenant nous avons interroger la base de donnée, dans ce chapitre il sera question d'insertion, de mise à jour et de suppression d'enregistrement.

Insertion (find_or_create)

Pour l'insertion nous utiliserons la methode find_or_create. Ajoutons un article (toujours à partir du controleur 'test':

my $article5=AppCDBI::Model::Exemple2DB::Article->find_or_create({
                                                                 id_article    => 5,
                                                                 page          => 1,
                                                                 titre         => 'Test insertion',
                                                                 date_creation => '2005-07-01',
                                                                 contenu       => 'Ceci est une insertion',
                                                                });

Et voilà un nouvel article est ajouté. On peut remarquer que cet article sera ajouté une seule fois.(même si on recharge la page)

Mise à jour d'enregistrement (update)

Pour permettre la mise à jour d'un enregistrement il nous faut dabord rechercher l'enregistrement (par un search ou search_like), modifier un/des champ(s) de celui-ci et ensuite faire l'update. Si nous souhaitons mettre à jour le titre article5 du paragraphe ci-dessus:

$article5->titre('Nouveau titre');
$article5->update;

Suppression d'enregistrement(s) (delete)

Pour supprimer un enregistrement il suffit d'utiliser la méthode delete. Toujours avec l'article5, nous ferions comme ceci:

$article5->delete

Avec la methode delete_all il est possible de supprimer plusieurs enregitrement à la fois.

 my @articles = AppCDBI::Model::Exemple2DB::Article->search( date_creation => '2005-12-10')->delete_all;

CDBI nous permet aussi la suppression en cascade, c'est à dire la suppression d'un (ou plusieurs) enregitrement et tout ce qu'il s'y réfère (voir has_a et has_many ). Nous pouvons par exemple supprimer une page et tous les articles qui s'y rattachent en supprimant simplement la page. CDBI se charge de tout :)

Mise en place d'un cache

A FAIRE ... voir http://catalyst.perl.org/calendar/2005/11

Des modules Catalyst

Catalyst-Plugin-UploadProgress

Il s'agit d'un plugin permettant de profiter d'une zolie barre de progression lors d'upload de fichiers :)

	localhost:/var/www/catalyst/# svn co http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Plugin-UploadProgress
	localhost:/var/www/catalyst/# cd Catalyst-Plugin-UploadProgress && perl Makefile.PL && make && make install
	localhost:/var/www/catalyst/Catalyst-Plugin-UploadProgress# cd example/Upload/
	localhost:/var/www/catalyst/Catalyst-Plugin-UploadProgress/example/Upload# perl script/upload_server.pl

Et voilà le résultat :)

Divers

use Catalyst;

my @auth    = qw/Authentication  
Authentication::Credential::Password Authentication::Store::Htpasswd/;
my @session = qw/Session Session::State:Cookie  
Session::Store::Memcached/;

__PACKAGE__->setup( @session, @auth );

Voir http://dev.catalyst.perl.org/file/trunk/Catalyst-Plugin-Authentication-Store-DBIC/README

Doc sur les CGI en Perl

Template Toolkit

TT is great for this.

Every template just does something like:
[% WRAPPER header.xhtml %]

page content here

[% END %]

And in header.xhtml you have it construct the page:

<!-- static header stuff -->

[% PROCESS left_menu.xhtml %]
[% PROCESS breadcrumb.xhtml %]

[% content %] <- page content is inserted here

[% PROCESS footer.xhtml %]

Catalyst et Oracle

I am researching the possibility of integrating Catalyst into my
company's high-traffic e-commerce site.  My firm uses Oracle as its
production database, so I am trying to get Catalyst and Oracle to work
together.  As a first stab, I am trying to get the MiniMojo Wiki from
the perl.com article (which I successfully set up) to use Oracle.
Here's what I've done so far...

*	Created a table called "page" in Oracle with the same structure
as the SqlLite version in the article.
*	Updated the MiniMojo::M::CDBI package to use dsn to connect to
our Oracle database.
*	Installed Class::DBI::Oracle and Class::DBI::Loader::Oracle
(since they didn't come with the Class::DBI or Catalyst packages).
*	Created an Oracle sequence called "page_seq" according to the
convention outlined in the Class::DBI::Oracle docs.
*	Added a constraint => '^page$' argument to the call to config()
in MiniMojo::M::CDBI, so it wouldn't try to load all of the tables in
the (very large) Oracle database.  (Is this the best/only way to specify
which tables to load?)
*	Browsed to http://localhost:3000 <http://localhost:3000/>  and
got the following error.

 

Caught exception "Can't insert new MiniMojo::M::CDBI::Page:
DBD::Oracle::st execute failed: ORA-01400: cannot insert NULL into
("CBD"."PAGE"."ID") (DBD ERROR: OCIStmtExecute) [for Statement "INSERT
INTO page (title, id) VALUES (?, ?) " with ParamValues: :p1='Frontpage',
:p2=undef] at
/usr/local/lib/perl5/site_perl/5.8.3/DBIx/ContextualFetch.pm line 51. at
/home/simon/catalyst/MiniMojo/script/../lib/MiniMojo/C/Page.pm line 41"

-------------------------------------------------------------------------------


Simon Miner wrote:
> Can anyone tell me why the Page class is not finding its sequence or 
> using this code?  How can I code logic and configuration specific to 
> this table?

Class::DBI::Loader::Oracle doesn't contain any sequence discovery code. 
  A coworker of mine wrote some but it hasn't made it into the 
Loader::Oracle distribution yet.  You can see the code as part of 
DBIx::Class though:
http://search.cpan.org/src/AGRUNDMA/DBIx-Class-0.02/lib/DBIx/Class/PK/Auto/Oracle.pm

But what you really want to do is simply forget about Class::DBI::Loader 
and define everything manually.  I've got 2 production Catalyst+Oracle 
systems running now and they run just fine with manually defined table 
classes.

Here's what our model classes look like for one of these apps:

package Events::M::CDBI;

use strict;
use base 'Class::DBI::Sweet';
use DateTime::Format::Strptime;
use Class::DBI::FromForm;

# For debugging, use this:
# $DBI::neat_maxlen = 2000;
# DBI->trace( 1, '/tmp/dbi.trace' );

# Get your $database, $schema, and $password values however you want

__PACKAGE__->connection(
     "dbi:Oracle:$database",
     $schema,
     $password,
     { LongReadLen => 65536, AutoCommit => 1 }
);

# cache all queries
# use this with care, on a fast server it will be a performance hit to 
have resultset_cache enabled globally

__PACKAGE__->cache( Events->cache );
__PACKAGE__->default_search_attributes( { use_resultset_cache => 1, 
profile_cache => 0 } );

# sequence support
__PACKAGE__->sequence( 'teched_events_seq_1' );
__PACKAGE__->set_sql( 'Nextval', 'SELECT %s.NEXTVAL from DUAL' );

# create all the date relationships
sub setup_date_fields {
     my $self = shift;
     foreach my $field ( $self->columns ) {
         if ( $field =~ /date$/i ) {
             $self->has_a(
                 $field => 'DateTime',
                 inflate => sub { $self->handle_date(shift) },
                 deflate => sub { shift->ymd },
             );
         }
     }
}

# handle dates, both from Oracle 16-JUN-05, and from the web YYYY-MM-DD
sub handle_date {
     my ( $self, $date ) = @_;

     if ($date =~ /\d{4}-\d{2}-\d{2}/) {
         return DateTime::Format::Strptime->new(
             pattern => '%Y-%m-%d',
             time_zone => 'America/New_York',
         )->parse_datetime( $date );
     } else {
         return DateTime::Format::Strptime->new(
             pattern => '%d-%b-%y',
             time_zone => 'America/New_York',
         )->parse_datetime( $date );
     }
}

1;

----

And here's one of the table classes.  Every table class follows the same 
basic pattern.  Note the alter session code.  This changes Oracle to use 
YYYY-MM-DD instead of the stupid DD-MON-YY format.  It's called in 
triggers because for whatever reason mod_perl connections don't seem to 
remember their date format.

package Events::M::Event;

use strict;
use base 'Events::M::CDBI';

__PACKAGE__->table('event');
__PACKAGE__->columns(Primary => qw/event_id/);
__PACKAGE__->columns(Essential => qw/title start_date end_date region_id
                                      location details_url comments 
enabled/);
__PACKAGE__->columns(Others => qw/create_date created_by modified_date 
modified_by/);

__PACKAGE__->setup_date_fields;

__PACKAGE__->has_a( region_id => 'Events::M::Region' );
__PACKAGE__->has_many( contacts => [ 'Events::M::EventContact' => 
'contact_id' ] );
__PACKAGE__->has_many( markets => [ 'Events::M::EventMarket' => 
'market_id' ] );
__PACKAGE__->has_many( product_groups => [ 
'Events::M::EventProductGroup' => 'product_group_id' ] );
__PACKAGE__->has_many( types => [ 'Events::M::EventType' => 'type_id' ] );

__PACKAGE__->add_trigger( before_create => \&alter_session );
__PACKAGE__->add_trigger( before_update => &alter_session );
__PACKAGE__->add_trigger( select => \&alter_session );

# Change the Oracle date format
# Every mod_perl process needs this to run at least once
sub alter_session {
     my $self = shift;

     # check the current nls_date_format
     my ($format) = $self->db_Main->selectrow_array( "SELECT value FROM 
sys.v_\$nls_parameters WHERE parameter = 'NLS_DATE_FORMAT'" );
     if ( $format ne "YYYY-MM-DD" ) {
         warn "NLS_DATE_FORMAT using incorrect value of $format, 
resetting to YYYY-MM-DD\n";
         $self->db_Main->do( 'alter session set nls_date_format = 
"YYYY-MM-DD"' );
     }
}

1;