Tutoriel (deuxième 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.

Nous nous sommes quittés après avoir créé une vue affichant les dernières bières ajoutées. Cette vue utilisait le helper link_to pour créer des liens vers la fiche de chaque bière :

<?= link_to($beer->nom, array('action' => 'view', 'id' => $beer->id)); ?>

Si vous affichez le HTML généré, vous vous apercevrez que les liens ressemblent à ça :

<a href="http://localhost/ilovebeer/beers/view/1">Eki</a>

En effet, nous avons vu précédemment que la route par défaut de Stato était :controller/:action/:id. Cette route signifie que chaque requête doit comporter un :controller et une :action à laquelle est fourni un :id (en général la clé primaire d'un enregistrement dans la base). Si nous faisons une requête vers l'URL ci-dessus, Stato va nous fournir dans le controller une propriété $params qui contiendra :

$params = array(
    'controller' => 'beers',
    'action'     => 'view',
    'id'         => 1
);

Vous êtes maintenant prêt à implémenter l'action view. Ouvrez le fichier app/controllers/beers_controller.php et ajoutez cette méthode :

public function view()
{
    $this->beer = Beer::$objects->get_or_404($this->params['id']);
}

Comme vous pouvez le constater, nous nous servons de la propriété $this→params du contrôleur pour récupérer l'ID demandée par l'utilisateur. La méthode get_or_404 est une méthode spéciale de la classe SManager : elle prend en paramètre l'ID de l'enregistrement à récupérer, et si elle ne le trouve pas, génère une exception SHttp404. Notez qu'il existe aussi une méthode get qui se contente de générer une exception SActiveRecordNotFound si elle ne trouve pas l'enregistrement, exception que vous devez “catcher” vous même. Nous allons voir dans quelques l'instants l'intérêt de générer une exception SHttp404. Voici le code du template, à placer dans un nouveau fichier app/views/beers/view.php :

<h2><?= $this->beer->nom; ?></h2>
<table>
    <tr>
        <th>Brasserie</th>
        <td><?= $this->beer->brasserie; ?></td>
    </tr>
    <tr>
        <th>% d'alcool</th>
        <td><?= $this->beer->degre_alcool; ?></td>
    </tr>
    <tr>
        <th>Couleur</th>
        <td><?= $this->beer->couleur; ?></td>
    </tr>
    <tr>
        <th>Goût</th>
        <td><?= $this->beer->gout; ?></td>
    </tr>
    <tr>
        <th>Fermentation</th>
        <td><?= $this->beer->fermentation; ?></td>
    </tr>
    <tr>
        <th>Description</th>
        <td><?= $this->beer->description; ?></td>
    </tr>
</table>

Rien de bien sorcier, aussi ne le commenterais-je pas. Vous pouvez également rajouter ces quelques lignes à la feuille de style basic.css :

th {
    text-align: left;
}
td {
    padding-left: 20px;
}

Il ne vous reste plus qu'à tester ! En cliquant sur l'un des liens, vous devriez obtenir ceci :

Revenons à notre exception SHttp404. Essayer de modifier l'URL en utilisant un ID qui n'existe pas encore dans la base, comme par exemple http://ilovebeer/beers/view/1234. Vous devriez obtenir l'erreur suivante :

SHttp404 in BeersController::view()

Quel intérêt me direz vous ? Cela ne semble pas très propre, mais il y a une raison à cela : nous sommes actuellement en environnment de développement, et donc Stato nous affiche toutes les erreurs. Nous allons donc tester le mode “production” ; pour cela, ouvrez le fichier conf/boot.php et décommentez la ligne suivante :

$_SERVER['STATO_ENV'] = 'production';

Si vous rafraîchissez la page, vous obtiendrez une page 404 plutôt sommaire, mais la bonne nouvelle est qu'elle est totalement modifiable : il vous suffit d'éditer le fichier public/404.html, de même qu'il existe un fichier 500.html pour couvrir tous les autres types d'erreurs en production. Pratique non ?

Création d'un premier formulaire

Il est temps maintenant d'offrir à nos futurs utilisateurs la possibilité d'ajouter de nouvelles bières. Pour cela, nous allons ajouter à notre contrôleur BeersController une action add. Une requête GET vers cette action entraînera l'affichage d'un formulaire de création, tandis qu'une requête POST (lors de la soumission du formulaire) permettra d'ajouter la nouvelle fiche dans la base (en cas de problème, par exemple un champ obligatoire manquant, le formulaire sera réaffiché avec la liste des erreurs). Pour commencer, nous allons ajouter au template de l'action index (app/views/beers/index.php) un lien vers l'action add :

<h2>Dernières bières dégustées</h2>
<?= link_to('Ajouter une bière', array('action' => 'add')); ?>
.....

Afin de nous faciliter la tâche de création du formulaire, nous allons utiliser la classe SFormBuilder. Cette classe a besoin d'une instance de la classe Beer pour fonctionner. Voici donc une première version de l'action add :

class BeersController extends ApplicationController
{
    ...
 
    public function add()
    {
        $this->beer = new Beer();
        $this->form = new SFormBuilder('new_beer', $this->beer);
    }
}

Créez un nouveau fichier app/views/beers/add.php avec le code suivant :

<h2>Ajouter une bière</h2>
<?= form_tag(array('action' => 'add')); ?>
    <?= $this->render_partial('form'); ?>
    <p class="submit">
        <?= submit_tag('Ajouter'); ?> ou <?= link_to('annuler', array('action' => 'index')); ?>
    </p>
</form>

Une nouveauté introduite par ce template est l'utilisation de render_partial : cette méthode permet de faire le rendu d'un sous-template et ainsi de réutiliser certaines portions de templates. L'idée ici est de réutiliser le corps du formulaire dans un futur formulaire d'édition. Par convention, le nom du “partial” doit être préfixé par un underscore. Il vous faut donc créer un fichier app/views/beers/_form.php contenant :

<?= $this->form->error_message(); ?>
<label for="new_beer_nom">Nom</label>
<?= $this->form->text_field('nom'); ?>
<label for="new_beer_brasserie">Brasserie</label>
<?= $this->form->text_field('brasserie'); ?>
<label for="new_beer_degre_alcool">% alcool</label>
<?= $this->form->text_field('degree_alcool', array('size' => 5)); ?>
<label for="new_beer_couleur">Couleur</label>
<?= $this->form->text_field('couleur'); ?>
<label for="new_beer_gout">Goût</label>
<?= $this->form->text_field('gout'); ?>
<label for="new_beer_fermentation">Fermentation</label>
<?= $this->form->text_field('fermentation'); ?>
<label for="new_beer_description">Description</label>
<?= $this->form->text_area('description', array('cols' => 80, 'rows' => 10)); ?>

Au sein de ce sous-template, nous disposons bien sûr de la propriété $this→form définie dans le contrôleur et contenant notre instance de SFormBuilder. Cette classe contient de nombreuses méthodes pour générer des éléments de formulaire à partir des propriétés d'une instance de SActiveRecord. Par exemple :

<?= $this->form->text_field('nom'); ?>

Va nous générer :

<input type="text" name="new_beer[nom]" id="new_beer_nom" size="30" />

Rappellez-vous en effet le premier paramètre que nous avons passé au constructeur de SFormBuilder : new_form. Ce paramètre va donc préfixer les attributs name et id de nos champs. L'attribut size contient ici une valeur par défaut, valeur que nous avons modifié pour le champ degree_alcool. Bien sûr, si l'instance de Beer que nous avons passé à SFormBuilder avait contenu des valeurs pour certains champs, ces valeurs auraient été affichées dans le formulaire.

Avant de tester le formulaire, je vous recommande de modifier votre feuille de style en y incluant les styles suivants :

label {
    display: block;
}
input, textarea, select {
    padding: 2px;
    border: 1px solid #ccc;
    margin-bottom: 10px;
}
p.submit {
    text-align: right;
}
p.submit input {
    margin-bottom: 0;
}
#form-errors {
    background-color: red;
    color: #fff;
    padding: 10px;
    margin: 20px 0;
}
#form-errors h3 {
    color: #fff;
}
#form-errors ul {
    padding-left: 15px;
}

Et si tout va bien, vous devriez obtenir ceci :

Notre formulaire est maintenant opérationnel, mais rien ne se passera si vous cliquez sur “Ajouter”, nous devons encore implémenter la sauvegarde de la fiche. Editez la méthode add de votre BeersController de la façon suivante :

class BeersController extends ApplicationController
{
    ...
 
    public function add()
    {
        if ($this->request->is_post()) {
            $this->beer = new Beer($this->params['new_beer']);
            if ($this->beer->save()) {
                $this->redirect_to(array('action' => 'view', 'id' => $this->beer->id));
                return;
            }
        } else {
            $this->beer = new Beer();
        }
        $this->form = new SFormBuilder('new_beer', $this->beer);
    }
}

Nous allons détailler ce code pas à pas. Tout d'abord, nous utilisons la méthode is_post de la classe SRequest (dont une instance est toujours disponible dans vos actions) pour déterminer si la requête est de type POST ou non. Si c'est le cas, et étant donné que tous les attributs name de nos champs de formulaire sont préfixés par new_beer, nous disposons dans $this→params d'un paramètre new_beer contenant un tableau des valeurs saisies dans le formulaire. Nous instancions donc un nouvel objet Beer avec ces valeurs, et nous essayons de le sauver. Si la méthode save() retourne true, nous pouvons alors rediriger l'utilisateur vers la page de visualisation de cette bière nouvellement créée. Il est très important de faire un return après un redirect_to afin de s'assurer que le reste du code de l'action n'est pas exécuté, ce qui pourrait poser problème. Si l'objet n'a pas pû être sauvé, l'instance de SFormBuilder que nous initialisons en dernière ligne contiendra cet objet avec les valeurs saisies par l'utilisateur et non un objet vierge, ce qui nous permet de réafficher le formulaire avec les valeurs déjà saisies.

Mais pourquoi l'objet ne pourrait-il pas être sauvé me direz-vous ? Tout simplement parce que la classe SActiveRecord vous permet d'effectuer certaines validations avant la sauvegarde de l'enregistrement. Ouvrez donc le fichier app/models/beer.php et ajoutez la méthode validate() ci-dessous :

class Beer extends SActiveRecord
{
    public static $objects;
    public $record_timestamps = true;
 
    public function validate()
    {
        $this->validate_presence_of('nom', 'Le champ %s est obligatoire');
    }
}

SActiveRecord définit en effet 3 methodes de validation surchargeables : validate(), qui est appellée lors de la sauvegarde d'un enregistrement, qu'il existe déjà dans la base ou non, ainsi que validate_on_create() et validate_on_update(). Nous nous contentons ici de rendre le champ nom obligatoire.

Afin d'afficher les erreurs générées lors de la validation, SFormBuilder dispose de la méthode error_message() que nous avons justement utilisé dans notre template. Essayez donc de soumettre le formulaire sans rien saisir et vous obtiendrez ceci :

C'est bien, mais pas top. Heureusement, error_message comme la plupart des helpers de Stato, accepte de nombreuses options ; modifiez la première ligne du partiel de formulaire comme ceci :

<?= $this->form->error_message(array(
        'header_tag' => 'h3',
        'header_text' => 'Veuillez corriger les erreurs ci-dessous'
)); ?>
<label for="new_beer_nom">Nom</label>
....

C'est beaucoup mieux. Nous pouvons encore améliorer notre formulaire en remplaçant certains champs texte par des select. Bien que n'étant pas expert en bière (plutôt un amateur éclairé), un petit tour sur Wikipédia m'a confirmé que le nombre de types de fermentation ainsi que le nombre de couleurs étaient limités. Modifiez donc votre formulaire de cette façon :

...
<label for="new_beer_couleur">Couleur</label>
<?= $this->form->select('couleur', array('blonde', 'ambrée', 'brune', 'noire', 'blanche')); ?>
...
<label for="new_beer_fermentation">Fermentation</label>
<?= $this->form->select('fermentation', array('basse', 'haute', 'spontanée', 'mixte')); ?>

Notre formulaire est maintenant complet et fonctionnel. Je vous propose donc de polir un peu notre application avant d'aller plus loin dans les fonctionnalités. Personnellement, je ne suis pas très fan des URLs du type http://ilovebeer/beers/view/214. Cet identifiant numérique n'est pas signifiant, pour ne pas dire très laid. Nous allons les remplacer par des URLs du type http://ilovebeer/beers/leffe_triple. Pour ce faire, nous devons stocker dans la base une version “simplifiée” du nom de la bière qui puisse être utilisé dans l'URL, ce qui signifie que nous allons devoir ajouter un champ dans la table beers. Si vous pouvez le faire à la main dans phpMyAdmin (ou tout autre outil), Stato vous propose une solution élégante : les migrations. En mode console (et en vous plaçant comme d'habitude dans le dossier de votre appli), exécutez la commande suivante :

php.exe scripts/do.php generate migration add_slug_column
    create db/migrate/1_add_slug_column.php

Ouvrez le fichier créé, et modifiez-le comme suit :

class AddSlugColumn extends SMigration
{
    public function up()
    {
        $this->add_column('beers', 'slug', 'string', array('after' => 'nom'));
        SMapper::reset_meta_information('Beer');
        foreach (Beer::$objects->all() as $beer) {
            $beer->update_attribute('slug', SInflection::urlize($beer->nom));
        }
    }
 
    public function down()
    {
        $this->remove_column('beers', 'slug');
    }
}

La méthode up() définit les modifications à opérer sur les tables, tandis que la méthode down() est là pour permettre d'annuler ces modifications. En effet, il est préférable de rendre les migrations réversibles. Si vous vous rendez compte une fois en production qu'il y a un problème avec votre code, vous pouvez ainsi revenir à une version antérieure de la base. Détaillons maintenant le code de la méthode up() :

  • add_column nous permet comme son nom l'indique d'ajouter notre nouvelle colonne que nous choisirons d'appeller slug.
  • la deuxième ligne réinitialise les informations de mapping de notre classe Beer. Ainsi le nouveau champ slug pourra être pris en compte dans la suite du code.
  • nous parcourons ensuite l'ensemble des enregistrements de la table en leur attribuant une valeur pour le champ slug, valeur obtenue grâce au helper SInflection::urlize() qui transforme tout mot ou phrase en une chaîne utilisable dans une URL.

Il ne vous reste plus qu'à exécuter la migration :

php.exe scripts/do.php migrate
Migrating to AddSlugColumn (1)
=> AddSlugColumn: migrating
=> AddSlugColumn: migrated (0.5758s)

Pour annuler la migration, il vous suffit d'exécuter la commande suivante :

php.exe scripts/do.php migrate -v 0
// ou bien :
php.exe scripts/do.php migrate --version=0
 
fr/tutoriel_part_2.txt · Last modified: 2009/06/15 18:02 by goldoraf
 
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