Tutoriel (première partie)

Ce tutoriel concerne la version de Stato en cours de développement (le trunk), qui peut être assez différente des versions précédentes.

L'objectif de ce tutoriel est de vous montrer comment créer une application web de A à Z avec le framework Stato. Nous allons donc développer ensemble un site de recommandation de bières, à la sauce 2.0 bien sûr. Il existe déjà au moins un site de ce type, Coastr, mais cela reste un concept plus fun à implémenter qu'un moteur de blog ;)

Préliminaires

Afin de suivre ce tutoriel, vous devez avoir un environnement de développement adéquat : consultez la doc concernant l'installation et créez un nouveau projet nommé ilovebeer (ou tout autre nom qui vous paraisse opportun ou “Safe at work”).

Hello World !

Si vous avez correctement créé votre appli comme indiqué dans les préliminaires, vous devez avoir un dossier ilovebeer/ à la racine de votre serveur web. Nous supposerons que vous avez créé le virtualhost correspondant.

L'essentiel du code devra être placé dans le sous-dossier app. Comme vous pourrez le constater, il possède une arborescence bien précise et c'est ce qui permet à Stato de s'y retrouver (“convention, not configuration”).

Détaillons un peu le contenu de ce dossier app :

  • controllers : c'est dans ce dossier que Stato ira chercher vos classes controller. Un controller répond à une requête faite par l'utilisateur.
  • i18n : comme son nom l'indique, ce dossier sert à stocker les données d'internationalisation de votre application.
  • views : ce dossier contiendra vos templates, qui génèreront le HTML retourné au navigateur.
  • models : ce dossier contiendra les classes qui modélisent et manipulent les données issues de votre base.
  • helpers : vous placerez ici les classes et fonctions assistant les autres composants (majoritairement les vues). On évite ainsi d'avoir trop de code procédural dans les templates ou les controllers.
  • resources : ce dossier sert à stocker les classes implémentant vos ressources RESTful, nous ne nous en occuperons pas.

En premier lieu, il est important de comprendre comment fonctionnent les Controllers et quelles relations ils entretiennent avec les URLs. L'URL d'une requête correspond à une classe controller et à une méthode de cette classe. Afin de procéder à quelques tests, nous allons créer un nouveau fichier nommé home_controller.php dans le dossier app/controllers avec le code suivant :

class HomeController extends ApplicationController
{
 
}

Si vous essayez dans votre navigateur l'url http://ilovebeer/, vous obtiendrez l'erreur suivante :

No dispatchable class identified !

Effectivement, vous n'avez pas précisé de controller dans l'URL, essayez donc avec http://ilovebeer/home :

Action index not found in HomeController

Ahah ! La partie home de l'url a bien été mappée vers notre nouveau controller, mais apparemment le framework cherche une action index dans ce controller. Nous allons donc l'implémenter :

class HomeController extends ApplicationController
{
    public function index()
    {
        $this->render_text('Hello World !');
    }
}

En rafraichissant la page, nous obtenons ceci :

Hello World !

Victoire ! Nous aurions bien sûr eu le même résultat avec l'url suivante : http://ilovebeer/home/index. En effet, si aucune action n'est précisée, le dispatcher recherche une action index. Ajoutons maintenant une nouvelle action :

class HomeController extends ApplicationController
{
    public function index()
    {
        $this->render_text('Hello World !');
    }
 
    public function test()
    {
        $this->render_text('Another action !');
    }
}

En utilisant l'url http://ilovebeer/home/test nous avons ceci :

Another action !

Vous devez maintenant avoir compris la relation entre l'URL et les controllers. Par défaut, les URLs sont de la forme :
http://host/:controller/:action/:id

Aperçu des fonctionnalités de l'application

Il est maintenant temps de détailler les principales fonctionnalités de notre application. L'idée principale est que tous les utilisateurs inscrits auront la possibilité de mettre en ligne des fiches relatives à un type de bière. Ces fiches sont uniquement descriptives (couleur, brasserie, degré d'alcool, etc…), le créateur d'une fiche ne donne pas son avis sur la bière. Par contre, les autres utilisateurs peuvent poster des commentaires, et surtout voter (“digger”) pour une bière qui leur plaît particulièrement. Les bières ayant le plus grand nombre de votes se retrouvent listées sur la page d'accueil. Nous ajouterons d'autres fonctionnalités par la suite, mais pour le moment cette description nous suffira pour commencer à construire notre modèle de données.

Modèle de données

Le modèle de données est assez simple : une table beers pour stocker les fiches descriptives, une table comments pour les commentaires des utilisateurs, et une table users pour les comptes utilisateurs. Enfin, afin de mémoriser les votes, nous allons ajouter une table votes, qui est une simple table de jointure pour ce qui est en fait une relation n-n (ou many_to_many) entre les tables users et beers.

Utilisez tout d'abord phpMyAdmin (ou tout autre outil) pour créer une base vide nommée ilovebeer. Profitez-en également pour créer un nouveau compte utilisateur : pour ma part, j'ai créé un compte trappiste avec le mot de passe cervoise.

Vous devez maintenant configurer Stato pour pouvoir accéder à votre base : ouvrez le fichier conf/database.php et modifiez les paramètres de façon à obtenir ceci :

$config = array
(
    'dev' => array
    (
        'adapter' => 'MySql',
        'host'    => 'localhost',
        'user'    => 'trappiste',
        'pass'    => 'cervoise',
        'dbname'  => 'ilovebeer'
    ),
    'prod' => array
    (
        'adapter' => 'MySql',
        'host'    => 'localhost',
        'user'    => 'trappiste',
        'pass'    => 'cervoise',
        'dbname'  => 'ilovebeer'
    ),
    'test' => array
    (
        'adapter' => 'MySql',
        'host'    => 'localhost',
        'user'    => 'trappiste',
        'pass'    => 'cervoise',
        'dbname'  => 'ilovebeer'
    )
);
 
return $config;

Stato vous permet d'utiliser différentes bases en fonction de l'environnement : développement, test, production. Nous nous contenterons d'utiliser la même base pour les 3.

Créez à présent un nouveau fichier db/db_schema.php avec le contenu suivant :

return array
(
    new STable('beers', array(
        new SColumn('id', 'primary_key'),
        new SColumn('nom', 'string'),
        new SColumn('brasserie', 'string'),
        new SColumn('degre_alcool', 'float'),
        new SColumn('couleur', 'string'),
        new SColumn('gout', 'string'),
        new SColumn('fermentation', 'string'),
        new SColumn('description', 'text'),
        new SColumn('created_on', 'datetime'),
        new SColumn('updated_on', 'datetime')
    )),
    new STable('comments', array(
        new SColumn('id', 'primary_key'),
        new SColumn('user_id', 'integer'),
        new SColumn('beer_id', 'integer'),
        new SColumn('body', 'text'),
        new SColumn('created_on', 'datetime')
    )),
    new STable('users', array(
        new SColumn('id', 'primary_key'),
        new SColumn('name', 'string'),
        new SColumn('email', 'string'),
        new SColumn('pwd', 'string'),
        new SColumn('created_on', 'datetime'),
        new SColumn('updated_on', 'datetime')
    )),
    new STable('votes', array(
        new SColumn('user_id', 'integer'),
        new SColumn('beer_id', 'integer')
    ))
);

Il ne nous reste plus qu'à laisser Stato créer les tables. Ouvrez une console, et placez vous dans votre répertoire ilovebeer/. Exécutez la commande suivante :

php.exe scripts/do.php db_schema_import
Importing DB schema...
    create table beers
    create table comments
    create table users
    create table votes

Les types suivants sont disponibles pour décrire le schéma de la base :

  • primary_key
  • integer
  • string
  • boolean
  • date
  • datetime
  • timestamp
  • float
  • text

Scaffolding

Il est pratique lorsque l'on développe une nouvelle application de disposer d'un moyen de saisir rapidement des données bidon de test, et une interface d'administration est l'endroit idéal pour ça. Nous allons donc y nous attaquer.

Pour créer le module admin, exécutez la commande suivante :

php.exe scripts/do.php generate module admin
    create modules
    create modules/admin
    create modules/admin/controllers
    create modules/admin/models
    create modules/admin/views
    create modules/admin/helpers
    create modules/admin/controllers/base_controller.php

Cette commande crée un nouveau dossier modules/admin à la racine du projet, contenant des sous-dossiers identiques aux sous-dossiers présents dans app. Il nous faut également créer une classe modèle pour encapsuler les enregistrements de la table beers. Créez un nouveau fichier app/models/beer.php avec le contenu suivant :

class Beer extends SActiveRecord
{
    public static $objects;
    public $record_timestamps = true;
}

C’est tout ce qu’il faut pour commencer à créer de nouveaux enregistrements dans la base. Nous n’avons même pas à préciser le nom de la table à utiliser : le framework va le “deviner”, et utiliser le pluriel du nom de la classe (classe Beer ⇒ table beers). Il va également créer automatiquement des propriétés dans cette classe permettant d’accéder aux champs de chaque enregistrement dans la table.

Enfin, nous allons exploiter la fonctionnalité de “scaffolding” (échafaudage) de Stato, avec la commande suivante :

php.exe scripts/do.php scaffold admin/beer
    create modules/admin/controllers/beers_controller.php
    create modules/admin/views/beers
    create modules/admin/views/beers/index.php
    create modules/admin/views/beers/view.php
    create modules/admin/views/beers/create.php
    create modules/admin/views/beers/update.php
    create modules/admin/views/beers/_form.php
    create views/layouts/scaffold.php

Stato va créer automatiquement un contrôleur et des vues de base autour de notre classe métier Beer. Le contrôleur contient des actions correspondant à toutes les opérations CRUD sur notre modèle, ce qui nous permet immédiatement de créer, éditer, lister et supprimer des posts dans la base !

Toutefois, si vous vous rendez tout de suite à l'URL http://ilovebeer/admin/beers/create, vous obtiendrez une erreur :

AdminController not found !

Et c'est normal : la création de notre module admin fait que le schéma par défaut des URLs (http://host/:controller/:action/:id) ne convient plus. Editez donc le fichier conf/routes.php de cette façon :

$map = new SRouteSet();
$map->connect('admin/:controller/:action/:id', array('module' => 'admin', 'controller' => 'beers'));
$map->connect(':controller/:action/:id');
 
return $map;

Si maintenant vous visitez l'URL http://ilovebeer/admin/beers/create, vous obtiendrez un formulaire de saisie similaire à l'image de gauche ci-dessous. Je vous invite donc à jouer avec votre nouvelle interface d'admin en créant quelques fiches de bières…

Front-end

En utilisant le scaffolding, nous avons pu très rapidement mettre en place une interface de gestion complète de nos bières. Les actions index, create, update et delete sont prêtes à l’emploi. Mais bien sûr, cette interface est très rustique : l’intérêt du scaffolding est de permettre la réalisation rapide de prototypes et de développer progressivement nos propres actions et views par dessus.

Nous allons maintenant revenir au front-end et créer une première page publique qui listera les dernières bières ajoutées par les utilisateurs. Pour cela, il nous faut d'abord un layout, c'est à dire un template de base pour toutes nos pages. Créez donc le fichier app/views/layouts/public.php avec le code suivant :

<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>I love beer : petites dégustations entre amis</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<?= stylesheet_link_tag('basic.css'); ?>
</head>
<body>
    <div id="header">
        <h1>I love beer...</h1>
    </div>
  	<div id="body">
        <?= $this->layout_content; ?>
    </div>
    <div id="footer">
        <p>Powered by Stato</p>
    </div>
</body>
</html>

Même s'il s'agit d'un layout, la syntaxe utilisée est la même que pour les templates. Nous pouvons insérer du code PHP dans le HTML en utilisant les tags <? ?> et <?= ?> si nous voulons générer du texte en sortie (l'équivalent d'un echo).

Remarque importante : Il ne s'agit pas des short tags de PHP !!! Les shorts tags ne sont pas appéciés des puristes, qui soutiennent que lorsque les navigateurs populaires supporteront vraiment xhtml (c.a.d accepteront le mime-type text/xhtml), ils pourraient entrer en conflit avec la déclaration XML <?xml version=“1.0” encoding=“UTF-8”?>. Bien qu'ils n'aient pas tout à fait tort sur le fond, certains ont prouvé que le problème pouvait être réglé en étant pris en compte par le parser PHP. La solution utilisée par Stato est de “précompiler” automatiquement les templates pour remplacer les shorts tags par les tags normaux. Vous n'avez donc pas besoin de l'option short_tags et le PHP utilisé lors du rendu du template contient des tags normaux.

Vous pouvez constater que nous avons inséré le code :

<?= stylesheet_link_tag('basic.css'); ?>

Il s'agit de notre première utilisation d'un helper de Stato : ce helper est une simple fonction qui générera le tag <link> nécessaire à l'inclusion de notre feuille de style.

Nous avons également écrit ceci :

<?= $this->layout_content; ?>

En effet, lors du rendu du layout, Stato va fournir le contenu du template dans cette variable $this→layout_content. Cela permet donc d'intégrer le rendu du template dans le layout.

Créons maintenant une feuille de style basique. Dans le dossier public/styles, créez un nouveau fichier basic.css contenant les styles suivants :

* {
    margin: 0;
    padding: 0;
    border: none;
}
body {
    font-family: Arial, Helvetica, sans-serif;
    font-size: 100%;
}
h1, h2, h3, h4 { 
    font-family: "Lucida Grande", Arial, Helvetica, sans-serif;
}
a, h2, h3, h4 { 
    color: #3F7F8D;
}
a:hover, a:active, a:focus { 
    color: #0092B3;
}
h2 {
    margin-bottom: 10px;
}
#header {
    background-color: #B34B00;
    color: #fff;
    padding: 20px;
}
#body {
    margin: 20px;
}
#footer {
    background-color: #8D603F;
    color: #fff;
    padding: 10px;
    text-align: center;
}
#beer-list {
    list-style: none;
}

Je ne rentrerais pas dans le détail de la feuille de style, car après tout ce n'est pas un tutoriel sur CSS… Nous allons maintenant pouvoir rentrer dans le vif du sujet et créer notre contrôleur BeersController :

php.exe scripts/do.php generate controller beers
    create controllers/beers_controller.php
    create views/beers

Editez le contrôleur créé de cette façon :

class BeersController extends ApplicationController
{
    protected $layout = 'public';
 
    public function index()
    {
        $this->beers = Beer::$objects->order_by('-created_on')->limit(20);
    }
}

Quand un utilisateur naviguera vers http://ilovebeer/beers/index, Stato va appeller la méthode index() que nous avons créée. Cette méthode, comme toutes les autres “actions”, a pour but de recueillir des données pour les passer au template. Afin de récupérer ces données, nous allons devoir faire une requête dans la base. Pour nous y aider, Stato affecte à chaque classe modèle une propriété statique $objects contenant une instance de la classe SManager. Cette classe possède différentes méthodes propres comme create qui permet d'instancier et sauver un nouvel objet, ou update, update_all, etc…, mais elle répond également aux méthodes de la classe SQuerySet en retournant une instance de cette classe. C'est cette classe SQuerySet qui permet de générer des requêtes SQL, via ses différentes méthodes filter, order_by, limit, etc… Chaque appel à une méthode de SQuerySet retourne une instance de SQuerySet, c'est ce qui nous permet de chaîner les appels. Un petit exemple pour illustrer tout ça :

$this->beers = Beer::$objects->filter("gout = 'douce'") // filter() est une méthode de SQuerySet
                                                        // comme la classe SManager ne la trouve pas (utilisation de __call)
                                                        // elle instancie un SQuerySet, appelle filter() et nous retourne l'instance
                             ->filter("degre_alcool > 5") // on ajoute un filtre supplémentaire, une nouvelle instance de SQuerySet nous est retournée
                             ->order_by("-created_on");

Mais revenons à nos moutons. Afin de rendre accessible le résultat de la requête dans le template, nous devons l’assigner à une variable d’instance du controller (ici $this→beers). Précisons toutefois que cette requête sera exécutée uniquement lorsque nous itèrerons la variable $this→beers à l’aide d’une boucle foreach, ce qui nous renverra des instances de la classe Beer. La méthode limit nous permet de ne renvoyer que les 20 dernières bières, et la méthode order_by de les ordonner par date de création décroissante (d’où le - devant created_on).

Nous allons maintenant créer le template (la vue) correspondant à notre action index : en effet, après avoir exécuté l’action demandée par l’utilisateur (index), Stato va aller chercher le template dans le sous-dossier de app/views correspondant au controller (en l’occurence app/views/beers) et utiliser le fichier portant le même nom que l’action (donc index.php). Nous devons donc créer un nouveau fichier app/views/beers/index.php contenant ceci :

<h2>Dernières bières dégustées</h2>
<ul id="beer-list">
<? foreach ($this->beers as $beer) : ?>
    <li>
        <?= link_to($beer->nom, array('action' => 'view', 'id' => $beer->id)); ?> 
        le <?= $beer->created_on->format('%d %B %Y - %H:%M'); ?>
    </li>
<? endforeach; ?>
</ul>

Dans notre template, nous itérons $this→beers (fourni par le controller) à travers une boucle foreach et nous affichons pour chaque objet certains de ses attributs.

Vous remarquerez l'utilisation de la méthode format('%d %B %Y - %H:%M:%S') sur l'attribut created_on. En effet, ce champ est de type DATETIME dans notre base, et ses valeurs sont donc automatiquement typées en tant qu'objet SDateTime par le framework. Si l'on avait utilisé simplement $beer→created_on, nous aurions obtenu la date sous la forme 2008-01-25 17:43:17. Avec la méthode format(), nous obtenons une forme plus lisible : 25 january 2008 - 17:43:17. Il nous suffit juste d'ajouter un appel à setlocale() dans le fichier conf/environment.php pour avoir la date en français :

setlocale(LC_TIME, 'fr_FR.utf8', 'fr_FR', 'fr');

Enfin, vous constaterez que nous utilisons le helper link_to qui permet comme son nom l'indique de générer un lien. Le tableau de paramètres que nous lui fournissons ne précise pas de contrôleur. Stato va donc utiliser le contrôleur courant.

Vous pouvez maintenant rafraîchir la page et contempler votre oeuvre :

Je vous invite maintenant à lire la deuxième partie du tutoriel.

 
fr/tutoriel_part_1.txt · Last modified: 2009/08/11 08:05 by damien.miras
 
Except where otherwise noted, content on this wiki is licensed under the following license:CC Attribution-Noncommercial-Share Alike 3.0 Unported
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki