Development

Documentation/fr_FR/book/1.0/trunk/10-Forms

You must first sign up to be able to contribute.

Version 48 (modified by berduj, 9 years ago)
--

Cette partie de la documentation est en cours de traduction. Cela signifie qu'elle est traduite de manière soit incomplète, soit inexacte. En attendant que cette traduction soit terminée, vous pouvez consulter la version en anglais pour des informations plus fiables.

Traducteur : Larson, début trad : 20/11/2007, fin prévue : 31/11/2007

Chapitre 10 - Les Formulaires

Lors de la création de templates, la majeure partie du développement est consacrée aux formulaires. Malgré cela, les formulaires sont généralement mal conçus. Beaucoup d'attention doit être apportée aux différents éléments du formulaire : les valeurs par défaut, le format, la validation, la re-saisie des champs, la prise en charge du formulaire en général. Certains développeurs ont tendance à oublier des étapes importantes de ce processus. Symfony apporte une attention toute particulière à ce sujet. Ce chapitre décrit les outils automatisant un grand nombre de ces exigences afin d'accélérer le temps de développement des formulaires :

  • Les Helpers de formulaire permettent d'afficher rapidement les champs du formulaire dans un template, en particulier pour les éléments complexes comme les dates, les menus déroulants, les textes enrichis.
  • Lorsqu'un formulaire est utilisé pour l'édition des propriétés d'un objet, l'écriture du template peut être accélérée grâce aux helpers de formulaire pour objets.
  • Le fichier de validation YAML facilite la validation et le remplissage automatique des champs du formulaire.
  • Les Validators regroupent le code nécessaire pour valider les données envoyées. Ce package Symfony est utilisable pour les besoins les plus courants, il peut néanmoins facilement être personnalisé.

Les Helpers de Formulaire

Dans les templates, les balises HTML des contrôles du formulaire sont généralement mélangés avec du code PHP. Le but des helpers de formulaire de symfony est de simplifier l'écriture des formulaires, et d'éviter d'ouvrir des <?php echo au milieu de balise <input>.

La Balise Form

Comme expliqué dans le chapitre précédent, vous devez utiliser l'assistant form_tag() pour créer un formulaire. Le premier argument définit la valeur de l'attribut action de la balise form, celle-ci est transformée en une URL. Le deuxième argument (optionnel) peut être une option supplémentaire (ex.: Modifier l'attribut method, enctype, ou spécifier d'autres attributs). Cf. Exemple 10-1.

*Exemple 10-1 - Le helper form_tag()*

    <?php echo form_tag('test/save') ?>
      => <form method="post" action="/path/to/save">

    <?php echo form_tag('test/save', 'method=get multipart=true class=simpleForm') ?>
      => <form method="get" enctype="multipart/form-data" class="simpleForm"action="/path/to/save">

Comme il est inutile d'avoir un helper de formulaire pour fermer le formulaire, vous devez utiliser la balise HTML </form>, même si ce n'est pas très joli dans le code source.

Les contrôles standards des formulaires

Avec les helpers de formulaire, tout les champs et contrôles ont un attribut id déduit de l'attribut name. Ce n'est pas la seule convention utile. L'exemple 10-2 présente la liste complète des helpers de formulaire standards et leurs options.

*Exemple 10-2 - Syntaxe des helpers de Formulaire Standard*

    // Champ Texte (input)
    <?php echo input_tag('nom', 'valeur par defaut') ?>
      => <input type="text" name="nom" id="nom" value="valeur par defaut" />

    // Tout les form helpers acceptent un paramètre optionnel supplémentaire
    // Il vous permet d'ajouter des attributs personnalisés dans la balise générique
    <?php echo input_tag('nom', 'valeur par defaut', 'maxlength=20') ?>
      => <input type="text" name="nom" id="nom" value="valeur par defaut"maxlength="20" />

    // Champ Texte Long (text area)
    <?php echo textarea_tag('nom', 'contenu par defaut', 'size=10x20') ?>
      => <textarea name="nom" id="nom" cols="10" rows="20">
           contenu par defaut
         </textarea>

    // Case à cocher (Check box)
    <?php echo checkbox_tag('celibataire', 1, true) ?>
    <?php echo checkbox_tag('permisdeconduire', 'B', false) ?>
      => <input type="checkbox" name="celibataire" id="celibataire" value="1"checked="checked" />
         <input type="checkbox" name="permisdeconduire" id="permisdeconduire"value="B" />

    // Radio button
    <?php echo radiobutton_tag('status[]', 'value1', true) ?>
    <?php echo radiobutton_tag('status[]', 'value2', false) ?>
      => <input type="radio" name="status[]" id="status_value1" value="value1"checked="checked" />
         <input type="radio" name="status[]" id="status_value2" value="value2" />

    // Menu Déroulant (select)
    <?php echo select_tag('payment',
      '<option selected="selected">Visa</option>
      <option>Eurocard</option>
      <option>Mastercard</option>')
    ?>
      => <select name="payment" id="payment">
           <option selected="selected">Visa</option>
           <option>Eurocard</option>
           <option>Mastercard</option>
         </select>

    // Liste des options pour la balise select
    <?php echo options_for_select(array('Visa', 'Eurocard', 'Mastercard'), 0) ?>
      => <option value="0" selected="selected">Visa</option>
         <option value="1">Eurocard</option>
         <option value="2">Mastercard</option>
 
    // Menu déroulant helper combiné avec la liste des options
    <?php echo select_tag('payment', options_for_select(array(
      'Visa',
      'Eurocard',
      'Mastercard'
    ), 0)) ?>
      => <select name="payment" id="payment">
           <option value="0" selected="selected">Visa</option>
           <option value="1">Eurocard</option>
           <option value="2">Mastercard</option>
         </select>
 
    // Pour spécifier l'attribut nom des options, utilisez un tableau associatif
    <?php echo select_tag('nom', options_for_select(array(
      'Nicolas'  => 'Nicolas',
      'Vincent'    => 'Vincent',
      'Claire' => 'Claire',
      'Alain'   => 'Alain',
      'Alexandre'    => 'Alexandre'
    ), 'Alain')) ?>
     => <select name="nom" id="nom">
          <option value="Nicolas">Nicolas</option>
          <option value="Vincent">Vincent</option>
          <option value="Claire">Claire</option>
          <option value="Alain" selected="selected">Alain</option>
          <option value="Alexandre">Alexandre</option>
        </select>
 
    // Menu Déroulant avec sélection multiple (les valeurs sélectionnés peuvent être un tableau)
    <?php echo select_tag('payment', options_for_select(
      array('Visa' => 'Visa', 'Eurocard' => 'Eurocard', 'Mastercard' => 'Mastercard'),
      array('Visa', 'Mastercard'),
    ), array('multiple' => true))) ?> 
     => <select name="payment[]" id="payment" multiple="multiple">
          <option value="Visa" selected="selected">Visa</option>
          <option value="Eurocard">Eurocard</option>
          <option value="Mastercard">Mastercard</option>
        </select>
    // Autre syntaxe
    <?php echo select_tag('payment', options_for_select(
      array('Visa' => 'Visa', 'Eurocard' => 'Eurocard', 'Mastercard' => 'Mastercard'),
      array('Visa', 'Mastercard')
    ), 'multiple=multiple') ?>
     => <select name="payment" id="payment" multiple="multiple">
          <option value="Visa" selected="selected"> 
          <option value="Eurocard">Eurocard</option>
          <option value="Mastercard" selected="selected">Mastercard</option>
        </select>
 
    // Champ de téléchargement d'un fichier
    <?php echo input_file_tag('nom') ?>
     => <input type="file" name="nom" id="nom" value="" />
 
    // Champ Mot de Passe
    <?php echo input_password_tag('nom', 'valeur') ?>
     => <input type="password" name="nom" id="nom" value="valeur" />
 
    // Champ caché
    <?php echo input_hidden_tag('nom', 'valeur') ?>
     => <input type="hidden" name="nom" id="nom" value="valeur" />
 
    // Bouton Submit (comme texte)
    <?php echo submit_tag('Sauvegarder') ?>
     => <input type="submit" name="submit" value="Sauvegarder" />
 
    // Bouton Submit (comme image)
    <?php echo submit_image_tag('submit_img') ?>
     => <input type="image" name="submit" src="/images/submit_img.png" />


Le helper submit_image_tag() utilise la même syntaxe et bénéficie des mêmes avantages que le helper image_tag().

**NOTE** Pour les boutons radio, l'attribut id est une combinaison des paramètres name et value, contrairement à la convention id=name normalement utilisée. Cela est du au fait que l'utilisation de plusieurs balises HTML dans la même page ayant le même attribut id est strictement interdite, par contre l'attribut name doit être identique pour permettre de ne sélectionner qu'un seul des boutons radio d'un groupe.

**SIDEBAR** Gérer la soumission d'un Formulaire Comment retrouver les données envoyées par les utilisateurs via un formulaire? Elles sont disponibles dans le paramètre request, donc il suffit à l'action d'appeler $this->getRequestParameter($elementName) pour obtenir la valeur. Une bonne pratique est d'utiliser la même action pour afficher et gérer le formulaire. En fonction de la méthode de soumission utilisée (GET ou POST), soit le template du formulaire est appelé, soit le formulaire est manager et la requête est redirigée vers une autre action.

<?php
    // Dans mymodule/actions/actions.class.php
    public function executeEditAuthor()
    {
      if ($this->getRequest()->getMethod() != sfRequest::POST)
      {
        // Affichage du Formulaire
        return sfView::SUCCESS;
      }
      else
      {
        // Management de la soumission du formulaire
        $name = $this->getRequestParameter('name');
        ...
        $this->redirect('mymodule/anotheraction');
      }
    }
     

Pour que cela fonctionne, le formulaire cible doit être la même action que celle qui l'affiche.

    // Dans mymodule/templates/editAuthorSuccess.php
    <?php echo form_tag('mymodule/editAuthor') ?>
    </form>

Symfony propose des helpers de formulaire spécialisés pour les requêtes asynchrones en tache de fond. Le chapitre suivant, centré sur l'AJAX, donne plus d'informations.

Les Gadgets pour les Champs Dates

Les champs dates sont souvent la cause d'erreur lors de la soumission de formulaire, généralement à cause d'un mauvais formatage. Symfony mets a votre disposition le helper input_date_tag() qui permet d'afficher un calendrier JavaScript interactif (Cf. Figure 10-1), facilitant la vie de l'utilisateur. Pour activer cette option, il faut ajouter le paramètre optionnel avec la valeur 'rich=true' lors de l'appel du helper.(Cf. Exemple 10-3)

Figure 10-1 - Calendrier JavaScript Interactif

Calendrier JavaScript Interactif

Si le paramètre optionnel rich est omit, le helper affiche alors trois champs <select> remplit avec un range de mois, jours, et années. On peut également afficher ces trois menus déroulants en appelant séparement leur helper (select_day_tag(), select_month_tag(), and select_year_tag()). La valeur par defaut de ces trois éléments est le jour, le mois et l'année courants. (Cf. Exemple 10-3)

Exemple 10-3 - Les Dates Helpers

    <?php echo input_date_tag('datedenaissance', '2005-05-03', 'rich=true') ?>
     => un champ texte avec le gadget calendrier (Figure 10-1)
 
    // Les helpers suivant nécessite le groupe de helper DateForm
    <?php use_helper('DateForm') ?>
 
    <?php echo select_day_tag('day', 1, 'include_custom=Choose a day') ?>
    => <select name="day" id="day">
          <option value="">Choose a day</option>
          <option value="1" selected="selected">01</option>
          <option value="2">02</option>
          ...
          <option value="31">31</option>
        </select>
 
    <?php echo select_month_tag('month', 1, 'include_custom=Choose a month use_short_month=true') ?>
    => <select name="month" id="month">
          <option value="">Choose a month</option>
          <option value="1" selected="selected">Jan</option>
          <option value="2">Feb</option>
          ...
          <option value="12">Dec</option>
        </select>
 
    <?php echo select_year_tag('year', 2007, 'include_custom=Choose a year year_end=2010') ?>
     => <select name="year" id="year">
          <option value="">Choose a year</option>
          <option value="2006">2006</option>
          <option value="2007" selected="selected">2007</option>
          ...
        </select>

Le helper input_date_tag() accepte comme valeur pour le paramètre date, les dates reconnues par la fonction PHP strtotime(). L'exemple 10-4 montre les formats qui peuvent être utilisés, L'Exemple 10-5 les formats interdits.

Exemple 10-4 - Formats de Date Acceptés par les Helpers Date

    // Fonctionne
    <?php echo input_date_tag('test', '2006-04-01', 'rich=true') ?>
    <?php echo input_date_tag('test', 1143884373, 'rich=true') ?>
    <?php echo input_date_tag('test', 'now', 'rich=true') ?>
    <?php echo input_date_tag('test', '23 October 2005', 'rich=true') ?>
    <?php echo input_date_tag('test', 'next tuesday', 'rich=true') ?>
    <?php echo input_date_tag('test', '1 week 2 days 4 hours 2 seconds', 'rich=true') ?>
 
    // Retourne null
    <?php echo input_date_tag('test', null, 'rich=true') ?>
    <?php echo input_date_tag('test', '', 'rich=true') ?>

Exemple 10-5 - Formats de Date Incorrect pour les Helpers Date

    // Date zero = 01/01/1970
    <?php echo input_date_tag('test', 0, 'rich=true') ?>
 
    // Les Formats de date non anglaise ne fonctionnent pas
    <?php echo input_date_tag('test', '01/04/2006', 'rich=true') ?>

Edition Rich Text

L'éditon Rich Text est possible dans une balise <textarea> graçe à l'intégration des gadgets TinyMCE et FCKEditor. Ils fournissent une "word-processor-like" interface avec des boutons pour formater le texte en gras, italique, et d'autres styles. Cf. Figure 10-2.

Figure 10-2 - Edition Rich Text

Edition Rich Text

Les deux gadgets nécessitent une installation manuelle. Les procédures d'installation sont identiques. Seule l'installation de TinyMCE est décrite içi. Vous devez télécharger l'éditeur sur le site du TinyMCE project (http://tinymce.moxiecode.com/) et le décompresser dans un dossier temporaire. Copiez the répertoire tinymce/jscripts/tiny_mce/ dans le répetoire web/js/ de votre projet. Définisez le chemin d'accès à la librairie dans settings.yml (Cf. Exemple 10-6).

Exemple 10-6 - Paramétrer le Path de la librairie TinyMCE

all:
  .settings:
    rich_text_js_dir:  js/tiny_mce

Une fois l'installation du gadget terminée, ajoutez la chaine rich=true dans le paramètre optionnel de helper textarea_tag pour utiliser celui-ci. Vous pouvez également ajouter des options personalisées dans ce paramètre destiné à l'éditeur JavaScript en utilisant l'option tinymce_options. (Cf. Exemple 10-7)

Exemple 10-7 - Editeur Rich Text

    <?php echo textarea_tag('name', 'default content', 'rich=true size=10x20')) ?>
     => L'éditeur Rich Text TinyMCE
    <?php echo textarea_tag('name', 'default content', 'rich=true size=10x20tinymce_options=language:"fr",theme_advanced_buttons2:"separator"')) ?>
    => L'éditeur de texte Rich Text TinyMCE avec des paramètres personalisées

Sélecteur de Pays et de Langage

Vous avez peut être également besoin d'un sélectionneur de pays. Mais comme le nom des pays diffères dans les différentes langues, les valeurs du menu déroulant doivent s'adapter à la culture de l'utilisateur ( Cf. Chapitre 13 pour plus d'information sur la notion de culture dans symfony). L'exemple 10-8 montre l'assistant select_country_tag() fait tout a votre place : Il internationalise le nom des pays et utilise le code standard ISO (2 caractères ex. "FR") du pays comme valeur. Exemple 10-8 - Le helper Sélecteur de Pays

    <?php echo select_country_tag('pays', 'AL') ?>
     => <select name="pays" id="pays">
          <option value="AF">Afghanistan</option>
          <option value="AL" selected="selected">Albania</option>
          <option value="DZ">Algeria</option>
          <option value="AS">American Samoa</option>
      ...

De la même manière que le helper select_country_tag(), le helper select_language_tag() affiche la liste des langues. (Cf. Exemple 10-9)

Exemple 10-9 - Le helper Sélecteur de Langue

    <?php echo select_language_tag('langue', 'en') ?>
     => <select name="langue" id="langue">
          ...
          <option value="elx">Elamite</option>
          <option value="en" selected="selected">English</option>
          <option value="enm">English, Middle (1100-1500)</option>
          <option value="ang">English, Old (ca.450-1100)</option>
          <option value="myv">Erzya</option>
          <option value="eo">Esperanto</option>
          ...

Helper Formulaire pour les Objets

Quand les élements d'un formulaire sont utilisés pour éditer les propriétés d'un objet, le helper standard de lien devient ennuyeux à écrire. Par exemple pour éditer la propriété telephone de l'objer Client, vous pouvez écrire ceci:

    <?php echo input_tag('telephone', $customer->getTelephone()) ?>
    => <input type="text" name="telephone" id="telephone" value="0123456789" />

Pour éviter de répeter l'attribut nom, symfony fournit le helper de formulaire objet pour chaque helper de formulaire. Un helper de formulaire objet déduit le nom et la valeur par défaut de l'élément du formulaire à partir d'un objet et du nom d'une méthode. L'exemple précédent peut également s'écrire sous la forme suivante :

    <?php echo object_input_tag($client, 'getTelephone') ?>
    => <input type="text" name="telephone" id="telephone" value="0123456789" />

L'économie peut paraître superflue pour l'assistant object_input_tag(). Cependant, tout les helpers de formulaire possèdent un helper de formulaire objet correspondant, utilisant la même syntaxe. Cela simplifie l'écriture des formulaires. Les helpers de formulaire objet sont considérablement utilisés dans les boucles et les tâches automatisées. L'exemple 10-10 présente la liste des helpers de formulaire objet.

Exemple 10-10 - Syntaxe des Assistant de Formulaire Objet

    <?php echo object_input_tag($object, $method, $options) ?>
    <?php echo object_input_date_tag($object, $method, $options) ?>
    <?php echo object_input_hidden_tag($object, $method, $options) ?>
    <?php echo object_textarea_tag($object, $method, $options) ?>
    <?php echo object_checkbox_tag($object, $method, $options) ?>
    <?php echo object_select_tag($object, $method, $options) ?>
    <?php echo object_select_country_tag($object, $method, $options) ?>
    <?php echo object_select_language_tag($object, $method, $options) ?>

Le helper object_password_tag() n'existe pas à cause de la mauvaise pratique d'attribuer une valeur par défaut à la balise password, basée sur la valeur que l'utilisateur avait précédemment saisie.

Contrairement aux helpers de formulaire standards, les helpers de formulaire objet sont utilisable uniquement si vous déclarez explicitement l'utilisation du groupe d'helpers Object dans votre template avec l'instruction use_helper('Object').

Les helpers de formulaire objet les plus intéressants sont objects_for_select() et object_select_tag(), pour les menus déroulants.

Remplir des Menus Déroulant avec des Objets

L' helper options_for_select() décrit précédement avec les autres helpers standard, transforme un tableau associatif PHP en une liste d'options, comme on peut le voir en figure 10-11.

Listing 10-11 - Créer une liste d'options à partir d'un tableau associatif avec options_for_select()

<?php echo options_for_select(array(
  '1' => 'Steve',
  '2' => 'Bob',
  '3' => 'Albert',
  '4' => 'Ian',
  '5' => 'Buck'
), 4) ?>
 => <option value="1">Steve</option>
    <option value="2">Bob</option>
    <option value="3">Albert</option>
    <option value="4" selected="selected">Ian</option>
    <option value="5">Buck</option>

Supposons que vous ayez déjà un tableau d'objets de la class Author, résultat d'un requète faite par Propel. Si vous voulez construire une liste d'options basée sur ce tableau, vous devrez boucler dessus pour récupérer l'id et le nom de chacun des objets comme on peut le voir en figure 10-12

Listing 10-12 - Créer une liste d'options basé sur un tableau d'objet options_for_select()

<?php
// In the action
$options = array();
foreach ($authors as $author)
{
  $options[$author->getId()] = $author->getName();
}
$this->options = $options;
 
// In the template
<?php echo options_for_select($options, 4) ?>

Ce genre de cas est fréquent, symfony donc a un helper pour l'automatiser : objects_for_select(),qui va créer une liste d'options basée directement sur un tableau d'objets. L' helper a besoin de deux paramètres additionnel : le nom de la méthode utilisée pour récupérer les valeurs et le texte affiché dans le tag <option>. Voir figure 10-12 :

<?php echo objects_for_select($authors, 'getId', 'getName', 4) ?>

C'est intelligent et rapide, mais symfony ne s'arrête pas là quand vous utilisez des clefs étrangères. Créer un menu déroulant basé sur une clef étrangère :

Fin de traduction pour le moment. En attendant que cette traduction soit terminée, vous pouvez consulter la version en anglais pour des informations plus fiables.

The values a foreign key column can take are the primary key values of the foreign table records. If, for instance, the article table has an author_id column that is a foreign key to an author table, the possible values for this column are the id of all the records of the author table. Basically, a drop-down list to edit the author of an article would look like Listing 10-13.

Listing 10-13 - Creating a List of Options Based on a Foreign Key with objects_for_select()

<?php echo select_tag('author_id', objects_for_select(
  AuthorPeer::doSelect(new Criteria()),
  'getId',
  '__toString',
  $article->getAuthorId()
)) ?>
=> <select name="author_id" id="author_id">
      <option value="1">Steve</option>
      <option value="2">Bob</option>
      <option value="3">Albert</option>
      <option value="4" selected="selected">Ian</option>
      <option value="5">Buck</option>
    </select>

The object_select_tag() does all that by itself. It displays a drop-down list populated with the name of the possible records of the foreign table. The helper can guess the foreign table and foreign column from the schema, so its syntax is very concise. Listing 10-13 is equivalent to this:

<?php echo object_select_tag($article, 'getAuthorId') ?>

The object_select_tag() helper guesses the related peer class name (AuthorPeer? in the example) based on the method name passed as a parameter. However, you can specify your own class by setting the related_class option in the third argument. The text content of the <option> tags is the record name, which is the result of the toString() method of the object class (if $author->toString() method is undefined, the primary key is used instead). In addition, the list of options is built from a doSelect() method with an empty criteria value; it returns all the records ordered by creation date. If you prefer to display only a subset of records with a specific ordering, create a method in the peer class returning this selection as an array of objects, and set it in the peer_method option. Lastly, you can add a blank option or a custom option at the top of the drop-down list by setting the include_blank and include_custom options. Listing 10-14 demonstrates these different options for the object_select_tag() helper.

Listing 10-14 - Options of the object_select_tag() Helper

// Base syntax
<?php echo object_select_tag($article, 'getAuthorId') ?>
// Builds the list from AuthorPeer::doSelect(new Criteria())
 
// Change the peer class used to retrieve the possible values
<?php echo object_select_tag($article, 'getAuthorId', 'related_class=Foobar') ?>
// Builds the list from FoobarPeer::doSelect(new Criteria())
 
// Change the peer method used to retrieve the possible values
<?php echo object_select_tag($article, 'getAuthorId','peer_method=getMostFamousAuthors') ?>
// Builds the list from AuthorPeer::getMostFamousAuthors(new Criteria())
 
// Add an <option value="">&nbsp;</option> at the top of the list
<?php echo object_select_tag($article, 'getAuthorId', 'include_blank=true') ?>
 
// Add an <option value="">Choose an author</option> at the top of the list
<?php echo object_select_tag($article, 'getAuthorId',
  'include_custom=Choose an author') ?>

Updating Objects

A form completely dedicated to editing object properties by using object helpers is easier to handle in an action. For instance, if you have an object of class Author with name, age, and address attributes, the form can be coded as shown in Listing 10-15.

Listing 10-15 - A Form with Only Object Helpers

<?php echo form_tag('author/update') ?>
  <?php echo object_input_hidden_tag($author, 'getId') ?>
  Name: <?php echo object_input_tag($author, 'getName') ?><br />
  Age:  <?php echo object_input_tag($author, 'getAge') ?><br />
  Address: <br />
         <?php echo object_textarea_tag($author, 'getAddress') ?>
</form>

The update action of the author module, called when the form is submitted, can simply update the object with the fromArray() modifier generated by Propel, as shown in Listing 10-16.

Listing 10-16 - Handling a Form Submission Based on Object Form Helpers

<?php
public function executeUpdate ()
{
  $author = AuthorPeer::retrieveByPk($this->getRequestParameter('id'));
  $this->forward404Unless($author);
 
  $author->fromArray($this->getRequest()->getParameterHolder()->getAll(),AuthorPeer::TYPE_FIELDNAME);
  $author->save();
 
  return $this->redirect('/author/show?id='.$author->getId());
}

Form Validation

Chapter 6 explained how to use the validateXXX() methods in the action class to validate the request parameters. However, if you use this technique to validate a form submission, you will end up rewriting the same portion of code over and over. Symfony provides an alternative form-validation technique, relying on only a YAML file, instead of PHP code in the action class.

To demonstrate the form-validation features, let's first consider the sample form shown in Listing 10-17. It is a classic contact form, with name, email, age, and message fields.

Listing 10-17 - Sample Contact Form, in modules/contact/templates/indexSuccess.php

<?php echo form_tag('contact/send') ?>
  Name:    <?php echo input_tag('name') ?><br />
  Email:   <?php echo input_tag('email') ?><br />
  Age:     <?php echo input_tag('age') ?><br />
  Message: <?php echo textarea_tag('message') ?><br />
  <?php echo submit_tag() ?>
</form>

The principle of form validation is that if a user enters invalid data and submits the form, the next page should show an error message. Let's define what valid data should be for the sample form, in plain English:

  • The name field is required. It must be a text entry between 2 and 100 characters.
  • The email field is required. It must be a text entry between 2 and 100 characters, and it must be a valid e-mail address.
  • The age field is required. It must be an integer between 0 and 120.
  • The message field is required.

You could define more complex validation rules for the contact form, but these are just fine for a demonstration of the validation possibilities.

Form validation can occur on the server side and/or on the client side. The server-side validation is compulsory to avoid corrupting a database with wrong data. The client-side validation is optional, though it greatly enhances the user experience. The client-side validation is to be done with custom JavaScript.

Validators

You can see that the name and email fields in the example share common validation rules. Some validation rules appear so often in web forms that symfony packages the PHP code that implements them into validators. A validator is simple class that provides an execute() method. This method expects the value of a field as parameter, and returns true if the value is valid and false otherwise.

Symfony ships with several validators (described in the "Standard Symfony Validators" section later in this chapter), but let's focus on the sfStringValidator for now. This validator checks that an input is a string, and that its size is between two specified character amounts (defined when calling the initialize() method). That's exactly what is required to validate the name field. Listing 10-18 shows how to use this validator in a validation method.

Listing 10-18 - Validating Request Parameters with Reusable Validators, in modules/contact/action/actions.class.php

<?php
public function validateSend()
{
  $name = $this->getRequestParameter('name');
 
  // The name field is required
  if (!$name)
  {
    $this->getRequest()->setError('name', 'The name field cannot be left blank');
 
    return false;
  }
 
  // The name field must be a text entry between 2 and 100 characters
  $myValidator = new sfStringValidator();
  $myValidator->initialize($this->getContext(), array(
    'min'       => 2,
    'min_error' => 'This name is too short (2 characters minimum)',
    'max'       => 100,
    'max_error' => 'This name is too long. (100 characters maximum)',
  ));
  if (!$myValidator->execute($name, $error))
  {
    return false;
  }
 
  return true;
}

If a user submits the form in Listing 10-17 with the value a in the name field, the execute() method of the sfStringValidator will return false (because the string length is less than the minimum of two characters). The validateSend() method will then fail, and the handleErrorSend() method will be called instead of the executeSend() method.

The setError() method of the sfRequest method gives information to the template so that it can display an error message (as explained in the "Displaying the Error Messages in the Form" section later in this chapter). The validators set the errors internally, so you can define different errors for the different cases of nonvalidation. That's the purpose of the min_error and max_error initialization parameters of the sfStringValidator.

All the rules defined in the example can be translated into validators:

  • name: sfStringValidator (min=2, max=100)
  • email: sfStringValidator (min=2, max=100) and sfEmailValidator
  • age: sfNumberValidator (min=0, max=120)

The fact that a field is required is not handled by a validator. Validation File

You could easily implement the validation of the contact form with validators in the validateSend() method PHP, but that would imply repeating a lot of code. Symfony offers an alternative way to define validation rules for a form, and it involves YAML. For instance, Listing 10-19 shows the translation of the name field validation rules, and its results are equivalent to those of Listing 10-18.

Listing 10-19 - Validation File, in modules/contact/validate/send.yml

fields:
  name:
    required:
      msg:       The name field cannot be left blank
    sfStringValidator:
      min:       2
      min_error: This name is too short (2 characters minimum)
      max:       100
      max_error: This name is too long. (100 characters maximum)

In a validation file, the fields header lists the fields that need to be validated, if they are required, and the validators that should be tested on them when a value is present. The parameters of each validator are the same as those you would use to initialize the validator manually. A field can be validated by as many validators as necessary.

The validation process doesn't stop when a validator fails. Symfony tests all the validators and declares the validation failed if at least one of them fails. And even if some of the rules of the validation file fail, symfony will still look for a validateXXX() method and execute it. So the two validation techniques are complementary. The advantage is that, in a form with multiple failures, all the error messages are shown.

Validation files are located in the module validate/ directory, and named by the action they must validate. For example, Listing 10-19 must be stored in a file called validate/send.yml. Redisplaying the Form

By default, symfony looks for a handleErrorSend() method in the action class whenever the validation process fails, or displays the sendError.php template if the method doesn't exist.

The usual way to inform the user of a failed validation is to display the form again with an error message. To that purpose, you need to override the handleErrorSend() method and end it with a redirection to the action that displays the form (in the example, module/index), as shown in Listing 10-20.

Listing 10-20 - Displaying the Form Again, in modules/contact/actions/actions.class.php

<?php
class ContactActions extends sfActions
{
  public function executeIndex()
  {
    // Display the form
  }
 
  public function handleErrorSend()
  {
    $this->forward('contact', 'index');
  }
 
  public function executeSend()
  {
    // Handle the form submission
  }
}

If you choose to use the same action to display the form and handle the form submission, then the handleErrorSend() method can simply return sfView::SUCCESS to redisplay the form from sendSuccess.php, as shown in Listing 10-21.

Listing 10-21 - A Single Action to Display and Handle the Form, in modules/contact/actions/actions.class.php

<?php
class ContactActions extends sfActions
{
  public function executeSend()
  {
    if ($this->getRequest()->getMethod() != sfRequest::POST)
    {
      // Prepare data for the template
 
      // Display the form
      return sfView::SUCCESS;
    }
    else
    {
      // Handle the form submission
      ...
      $this->redirect('mymodule/anotheraction');
    }
  }
  public function handleErrorSend()
  {
    // Prepare data for the template
 
    // Display the form
    return sfView::SUCCESS;
  }
}

The logic necessary to prepare the data can be refactored into a protected method of the action class, to avoid repeating it in the executeSend() and handleErrorSend() methods.

With this new configuration, when the user types an invalid name, the form is displayed again, but the entered data is lost and no error message explains the reason of the failure. To address the last issue, you must modify the template that displays the form, to insert error messages close to the faulty field. Displaying the Error Messages in the Form

The error messages defined as validator parameters are added to the request when a field fails validation (just as you can add an error manually with the setError() method, as in Listing 10-18). The sfRequest object provides two useful methods to retrieve the error message: hasError() and getError(), which each expect a field name as parameter. In addition, you can display an alert at the top of the form to draw attention to the fact that one or many of the fields contain invalid data with the hasErrors() method. Listings 10-22 and 10-23 demonstrate how to use these methods.

Listing 10-22 - Displaying Error Messages at the Top of the Form, in templates/indexSuccess.php

<?php if ($sf_request->hasErrors()): ?>
  <p>The data you entered seems to be incorrect.
  Please correct the following errors and resubmit:</p>
  <ul>
  <?php foreach($sf_request->getErrors() as $name => $error): ?>
    <li><?php echo $name ?>: <?php echo $error ?></li>
  <?php endforeach; ?>
  </ul>
<?php endif; ?>

Listing 10-23 - Displaying Error Messages Inside the Form, in templates/indexSuccess.php

<?php echo form_tag('contact/send') ?>
  <?php if ($sf_request->hasError('name')): ?>
    <?php echo $sf_request->getError('name') ?> <br />
  <?php endif; ?>
  Name:    <?php echo input_tag('name') ?><br />
  ...
  <?php echo submit_tag() ?>
</form>

The conditional use of the getError() method in Listing 10-23 is a bit long to write. That's why symfony offers a form_error() helper to replace it, provided that you declare the use of its helper group, Validation. Listing 10-24 replaces Listing 10-23 by using this helper.

Listing 10-24 - Displaying Error Messages Inside the Form, the Short Way

<?php use_helper('Validation') ?>
<?php echo form_tag('contact/send') ?>
 
           <?php echo form_error('name') ?><br />
  Name:    <?php echo input_tag('name') ?><br />
  ...
  <?php echo submit_tag() ?>
</form>

The form_error() helper adds a special character before and after each error message to make the messages more visible. By default, the character is an arrow pointing down (corresponding to the &darr; entity), but you can change it in the settings.yml file:

all:
  .settings:
    validation_error_prefix:    ' &darr;&nbsp;'
    validation_error_suffix:    ' &nbsp;&darr;'

In case of failed validation, the form now displays errors correctly, but the data entered by the user is lost. You need to repopulate the form to make it really user-friendly. Repopulating the Form

As the error handling is done through the forward() method (shown in Listing 10-20), the original request is still accessible, and the data entered by the user is in the request parameters. So you could repopulate the form by adding default values to each field, as shown in Listing 10-25.

Listing 10-25 - Setting Default Values to Repopulate the Form When Validation Fails, in templates/indexSuccess.php

<?php use_helper('Validation') ?>
<?php echo form_tag('contact/send') ?>
           <?php echo form_error('name') ?><br />
  Name:    <?php echo input_tag('name', $sf_params->get('name')) ?><br />
           <?php echo form_error('email') ?><br />
  Email:   <?php echo input_tag('email', $sf_params->get('email')) ?><br />
           <?php echo form_error('age') ?><br />
  Age:     <?php echo input_tag('age', $sf_params->get('age')) ?><br />
           <?php echo form_error('message') ?><br />
  Message: <?php echo textarea_tag('message', $sf_params->get('message')) ?><br />
  <?php echo submit_tag() ?>
</form>

But once again, this is quite tedious to write. Symfony provides an alternative way of triggering repopulation for all the fields of a form, directly in the YAML validation file, without changing the default values of the elements. Just enable the fillin: feature for the form, with the syntax described in Listing 10-26.

Listing 10-26 - Activating fillin to Repopulate the Form When Validation Fails, in validate/send.yml

fillin:
  enabled: true  # Enable the form repopulation
  param:
    name: test  # Form name, not needed if there is only one form in the page
    skip_fields:   [email]  # Do not repopulate these fields
    exclude_types: [hidden, password] # Do not repopulate these field types
    check_types:   [text, checkbox, radio, password, hidden] # Do repopulate these

By default, the automatic repopulation works for text inputs, check boxes, radio buttons, text areas, and select components (simple and multiple), but it does not repopulate password or hidden tags. The fillin feature doesn't work for file tags.

The fillin feature works by parsing the response content in XML just before sending it to the user. If the response is not a valid XHTML document, fillin might not work.

You might want to transform the values entered by the user before writing them back in a form input. Escaping, URL rewriting, transformation of special characters into entities, and all the other transformations that can be called through a function can be applied to the fields of your form if you define the transformation under the converters: key, as shown in Listing 10-27.

Listing 10-27 - Converting Input Before fillin, in validate/send.yml

fillin:
  enabled: true
  param:
    name: test
    converters:         # Converters to apply
      htmlentities:     [first_name, comments]
      htmlspecialchars: [comments]

Standard Symfony Validators

Symfony contains some standard validators that can be used for your forms:

  • sfStringValidator
  • sfNumberValidator
  • sfEmailValidator
  • sfUrlValidator
  • sfRegexValidator
  • sfCompareValidator
  • sfPropelUniqueValidator
  • sfFileValidator
  • sfCallbackValidator

Each has a default set of parameters and error messages, but you can easily override them through the initialize() validator method or in the YAML file. The following sections describe the validators and show usage examples. String Validator

sfStringValidator allows you to apply string-related constraints to a parameter.

sfStringValidator:
  values:       [foo, bar]
  values_error: The only accepted values are foo and bar
  insensitive:  false  # If true, comparison with values is case insensitive
  min:          2
  min_error:    Please enter at least 2 characters
  max:          100
  max_error:    Please enter less than 100 characters

Number Validator

sfNumberValidator verifies if a parameter is a number and allows you to apply size constraints.

sfNumberValidator:
  nan_error:    Please enter an integer
  min:          0
  min_error:    The value must be at least zero
  max:          100
  max_error:    The value must be less than or equal to 100

E-Mail Validator

sfEmailValidator verifies if a parameter contains a value that qualifies as an e-mail address.

sfEmailValidator:
  strict:       true
  email_error:  This email address is invalid

RFC822 defines the format of e-mail addresses. However, it is more permissive than the generally accepted format. For instance, me@localhost is a valid e-mail address according to the RFC, but you probably don't want to accept it. When the strict parameter is set to true (its default value), only e-mail addresses matching the pattern name@domain.extension are valid. When set to false, RFC822 is used as a rule. URL Validator

sfUrlValidator checks if a field is a correct URL.

sfUrlValidator:
  url_error:    This URL is invalid

Regular Expression Validator

sfRegexValidator allows you to match a value against a Perl-compatible regular expression pattern.

sfRegexValidator:
  match:        No
  match_error:  Posts containing more than one URL are considered as spam
  pattern:      /http.*http/si

The match parameter determines if the request parameter must match the pattern to be valid (value Yes) or match the pattern to be invalid (value No). Compare Validator

sfCompareValidator checks the equality of two different request parameters. It is very useful for password checks.

fields:
  password1:
    required:
      msg:      Please enter a password
  password2:
    required:
      msg:      Please retype the password
    sfCompareValidator:
      check:    password1
      compare_error: The two passwords do not match

The check parameter contains the name of the field that the current field must match to be valid. Propel Unique Validator

sfPropelUniqueValidator validates that the value of a request parameter doesn't already exist in your database. It is very useful for unique indexes.

fields:
  nickname:
    sfPropelUniqueValidator:
      class:        User
      column:       login
      unique_error: This login already exists. Please choose another one.

In this example, the validator will look in the database for a record of class User where the login column has the same value as the field to validate. File Validator

sfFileValidator applies format (an array of mime-types) and size constraints to file upload fields.

fields:
  image:
    file:       True
    required:
      msg:      Please upload an image file
    sfFileValidator:
      mime_types:
        - 'image/jpeg'
        - 'image/png'
        - 'image/x-png'
        - 'image/pjpeg'
      mime_types_error: Only PNG and JPEG images are allowed
      max_size:         512000
      max_size_error:   Max size is 512Kb

Be aware that the file attribute must be set to True for the field, and the template must declare the form as multipart. Callback Validator

sfCallbackValidator delegates the validation to a third-party callable method or function to do the validation. The callable method or function must return true or false.

fields:
  account_number:
    sfCallbackValidator:
      callback:      is_numeric
      invalid_error: Please enter a number.
  credit_card_number:
    sfCallbackValidator:
      callback:      [myTools, validateCreditCard]
      invalid_error: Please enter a valid credit card number.

The callback method or function receives the value to be validated as a first parameter. This is very useful when you want to reuse existing methods of functions, rather than create a full validator class.

You can also write your own validators, as described in the "Creating a Custom Validator" section later in this chapter.

Named Validators

If you see that you need to repeat a validator class and its settings, you can package it under a named validator. In the example of the contact form, the email field needs the same sfStringValidator parameters as the name field. So you can create a myStringValidator named validator to avoid repeating the same settings twice. To do so, add a myStringValidator label under the validators: header, and set the class and param keys with the details of the named validator you want to package. You can then use the named validator just like a regular one in the fields section, as shown in Listing 10-28.

Listing 10-28 - Reusing Named Validators in a Validation File, in validate/send.yml

validators:
  myStringValidator:
    class: sfStringValidator
    param:
      min:       2
      min_error: This field is too short (2 characters minimum)
      max:       100
      max_error: This field is too long (100 characters maximum)

fields:
  name:
    required:
      msg:       The name field cannot be left blank
    myStringValidator:
  email:
    required:
      msg:       The email field cannot be left blank
    myStringValidator:
    sfEmailValidator:
      email_error:  This email address is invalid

Restricting the Validation to a Method

By default, the validators set in a validation file are run when the action is called with the POST method. You can override this setting globally or field by field by specifying another value in the methods key, to allow a different validation for different methods, as shown in Listing 10-29.

Listing 10-29 - Defining When to Test a Field, in validate/send.yml

methods:         [post]     # This is the default setting

fields:
  name:
    required:
      msg:       The name field cannot be left blank
    myStringValidator:
  email:
    methods:     [post, get] # Overrides the global methods settings
    required:
      msg:       The email field cannot be left blank
    myStringValidator:
    sfEmailValidator:
      email_error:  This email address is invalid

What Does a Validation File Look Like?

So far, you have seen only bits and pieces of a validation file. When you put everything together, the validation rules find a clear translation in YAML. Listing 10-30 shows the complete validation file for the sample contact form, corresponding to all the rules defined earlier in the chapter.

Listing 10-30 - Sample Complete Validation File

fillin:
  enabled:      true

validators:
  myStringValidator:
    class: sfStringValidator
    param:
      min:       2
      min_error: This field is too short (2 characters minimum)
      max:       100
      max_error: This field is too long (100 characters maximum)

fields:
  name:
    required:
      msg:       The name field cannot be left blank
    myStringValidator:
  email:
    required:
      msg:       The email field cannot be left blank
    myStringValidator:
    sfEmailValidator:
      email_error:  This email address is invalid
  age:
    sfNumberValidator
      nan_error:    Please enter an integer
      min:          0
      min_error:    "You're not even born. How do you want to send a message?"
      max:          120
      max_error:    "Hey, grandma, aren't you too old to surf on the Internet?"
  message:
    required:
      msg:          The message field cannot be left blank

Complex Validation

The validation file satisfies most needs, but when the validation is very complex, it might not be sufficient. In this case, you can still return to the validateXXX() method in the action, or find the solution to your problem in the following sections. Creating a Custom Validator

Each validator is a class that extends the sfValidator class. If the validator classes shipped with symfony are not suitable for your needs, you can easily create a new one, in any of the lib/ directories where it can be autoloaded. The syntax is quite simple: The execute() method of the validator is called when the validator is executed. You can also define default settings in the initialize() method.

The execute() method receives the value to validate as the first parameter and the error message to throw as the second parameter. Both are passed as references, so you can modify the error message from within the method.

The initialize() method receives the context singleton and the array of parameters from the YAML file. It must first call the initialize() method of its parent sfValidator class, and then set the default values.

Every validator has a parameter holder accessible by $this->getParameterHolder().

For instance, if you want to build an sfSpamValidator to check if a string is not spam, add the code shown in Listing 10-31 to an sfSpamValidator.class.php file. It checks if the $value contains more than max_url times the string 'http'.

Listing 10-31 - Creating a Custom Validator, in lib/sfSpamValidator.class.php

<?php
class sfSpamValidator extends sfValidator
{
  public function execute (&$value, &$error)
  {
    // For max_url=2, the regexp is /http.*http/is
    $re = '/'.implode('.*', array_fill(0, $this->getParameter('max_url') + 1, 'http')).'/is';
 
    if (preg_match($re, $value))
    {
      $error = $this->getParameter('spam_error');
 
      return false;
    }
 
    return true;
  }
 
  public function initialize ($context, $parameters = null)
  {
    // Initialize parent
    parent::initialize($context);
 
    // Set default parameters value
    $this->setParameter('max_url', 2);
    $this->setParameter('spam_error', 'This is spam');
 
    // Set parameters
    $this->getParameterHolder()->add($parameters);
 
    return true;
  }
}

As soon as the validator is added to an autoloadable directory (and the cache cleared), you can use it in your validation files, as shown in Listing 10-32.

Listing 10-32 - Using a Custom Validator, in validate/send.yml

fields:
  message:
    required:
      msg:          The message field cannot be left blank
    sfSpamValidator:
      max_url:      3
      spam_error:   Leave this site immediately, you filthy spammer!

Using Array Syntax for Form Fields

PHP allows you to use an array syntax for the form fields. When writing your own forms, or when using the ones generated by the Propel administration (seeChapter 14), you may end up with HTML code that looks like Listing 10-33.

Listing 10-33 - Form with Array Syntax

Using the input name as is (with brackets) in a validation file will throw a parsed-induced error. The solution here is to replace square brackets [] with curly brackets {} in the fields section, as shown in Listing 10-34, and symfony will take care of the conversion of the names sent to the validators afterwards.

Listing 10-34 - Validation File for a Form with Array Syntax

fields:
  story{title}:
    required:     Yes

Executing a Validator on an Empty Field

You may need to execute a validator on a field that is not required, on an empty value. For instance, this happens with a form where the user can (but may not) want to change his password, and in this case, a confirmation password must be entered. See the example in Listing 10-35.

Listing 10-35 - Sample Validation File for a Form with Two Password Fields

fields:
  password1:
  password2:
    sfCompareValidator:
      check:         password1
      compare_error: The two passwords do not match

The validation process executes as follows:

*

If password1 == null and password2 == null:

o The required test passes. o Validators are not run. o The form is valid.

*

If password2 == null while password1 is not null:

o The required test passes. o Validators are not run. o The form is valid.

You may want to execute your password2 validator if password1 is not null. Fortunately, the symfony validators handle this case, thanks to the group parameter. When a field is in a group, its validator will execute if it is not empty and if one of the fields of the same group is not empty.

So, if you change the configuration to that shown in Listing 10-36, the validation process behaves correctly.

Listing 10-36 - Sample Validation File for a Form with Two Password Fields and a Group

fields:c
  password1:
    group:           password_group
  password2:
    group:           password_group
    sfCompareValidator:
      check:         password1
      compare_error: The two passwords do not match

The validation process now executes as follows:

*

If password1 == null and password2 == null:

o The required test passes. o Validators are not run. o The form is valid.

*

If password1 == null and password2 == 'foo':

o The required test passes. o password2 is not null, so its validator is executed, and it fails. o An error message is thrown for password2.

*

If password1 == 'foo' and password2 == null:

o The required test passes. o password1 is not null, so the validator for password2, which is in the same group, is executed, and it fails. o An error message is thrown for password2.

*

If password1 == 'foo' and password2 == 'foo':

o The required test passes. o password2 is not null, so its validator is executed, and it passes. o The form is valid.

Summary

Writing forms in symfony templates is facilitated by the standard form helpers and their smart options. When you design a form to edit the properties of an object, the object form helpers simplify the task a great deal. The validation files, validation helpers, and repopulation feature reduce the work necessary to build a robust and user-friendly server control on the value of a field. And even the most complex validation needs can be handled, either by writing a custom validator or by creating a validateXXX() method in the action class. Chapter 11 - Ajax Integration » « Chapter 9 - Links And The Routing System Questions & Feedback

If you find a typo or an error, please open a ticket.

If you need support or have a technical question, please post to the user mailing-list or to the forum. Contacts & Credits - Make a donation - Powered by Symfony_button - Sponsored by Sensio_labs_button