Symfony 1.1 beta, tour du propriétaire - Les formulaires
Par NiKo le mardi 11 mars 2008, 10:50 - Dev
- Lien permanent -
12 commentaires -
Tags :
Nous venons de voir la procédure d'installation de la beta1 de Symfony 1.1. Nous allons maintenant rentrer un peu plus dans les détails des nouvelles fonctionnalités en commençant par les formulaires.
La gestion des formulaires avec Symfony 1.1
La nouvelle gestion des formulaires est l'une des fonctionnalités majeures de cette nouvelle mouture du framework. Elle propose une séparation claire entre couche de contrôle, couche de définition et couche de présentation des données, soit un bon vieux pattern MVC des familles.
Pour illustrer ces fonctionnalités, nous allons retrousser nos manches et créer un formulaire de contact basique. On commence par initialiser un nouveau module contact dans l'application main de notre projet sf11test initié précédemment :
$ ./symfony generate:module main contact
Création d'une classe de formulaire
Nous allons créer une classe qui représentera notre formulaire de contact, que nous stockerons dans le fichier apps/main/lib/ContactForm.class.php. Cette classe étendra la classe de base sfForm et surchargera sa méthode de configuration afin de définir les champs de formulaire et les différents validateurs associés. Nous définirons quatre champs (appelés widgets en Symfony 1.1) :
- Le nom de l'expéditeur, sous la forme d'un champs de saisie textuelle (
<input type="text"/>) - Son adresse email, également sous la forme d'un champs de saisie textuelle
- Le sujet de son message, sous la forme d'une boîte de sélection (
<select/>) - Le texte de son message, sous la forme d'un champs texte multilignes (
<textarea/>)
<?php class ContactForm extends sfForm { public function configure() { // Widgets $topics = sfConfig::get('app_contact_topics', array()); $widgetSchema = new sfWidgetFormSchema(array( 'topic' => new sfWidgetFormSelect(array('choices' => $topics)), 'name' => new sfWidgetFormInput(), 'email' => new sfWidgetFormInput(), 'message' => new sfWidgetFormTextarea() )); $widgetSchema->setNameFormat('contact[%s]'); // HTML field names format $this->setWidgetSchema($widgetSchema); // Validators $this->setValidators(array( 'topic' => new sfValidatorRegex(array('pattern' => '/[a-z_]/')), 'name' => new sfValidatorString(array('min_length' => 2, 'max_length' => 45)), 'email' => new sfValidatorAnd(array(new sfValidatorEmail(), new sfValidatorString(array('max_length' => 100)))), 'message' => new sfValidatorString(array('min_length' => 10)), )); } }
Au passage, nous ajoutons les sujets possibles de message dans le fichier de configuration apps/main/config/app.yml :
all:
contact:
topics:
carrots_request: Do you have carrots?
eggs_request: Do you have eggs?
Marquons un arrêt pour examiner de plus près ce que nous venons d'écrire :
- Nous avons ajouté 4 widgets à notre formulaires,
- Nous avons défini et paramétré leurs validateurs associés,
- Au passage, vous noterez qu'il est possible de spécifier plusieurs validateurs pour un même champs, comme c'est ici le cas pour le champs
email
- Au passage, vous noterez qu'il est possible de spécifier plusieurs validateurs pour un même champs, comme c'est ici le cas pour le champs
- Nous avons défini le format de nommage des champs de formulaire et choisi une syntaxe à base de tableau pour manipuler plus aisément les données dans notre action,
- Nous avons déporté une partie de la configuration textuelle dans un fichier externe dédié, pour en faciliter la maintenance.
N'oublions pas de purger le cache de Symfony, car nous venons d'ajouter un nouvel objet php :
$ ./symfony cc
Interaction avec le formulaire depuis le contrôleur de l'application
Éditons maintenant le fichier apps/main/modules/contact/actions/actions.class.php pour y définir l'action par défaut du module[1] :
<?php class contactActions extends sfActions { public function executeIndex(sfWebRequest $request) { $form = new ContactForm(); if ($request->isMethod('post')) // If HTTP method is POST { // Bind submitted values to the contact form instance $form->bind($request->getParameter('contact')); // Validate the form if ($form->isValid()) { // Retrieve submitted values $values = $form->getValues(); // ... Send your message here using submitted values // Then redirect user to the homepage with a one-shot message $this->getUser()->setFlash('notice', 'Message sent'); $this->redirect('@homepage'); } } // Publish form instance to the view $this->form = $form; } }
Ici, dans l'ordre :
- On instancie un objet de formulaire de contact
- Si la méthode HTTP est POST :
- On assigne les paramètres de la requête correspondant aux champs du formulaire de contact à ce dernier
- On lance la validation, et si c'est valide :
- On effectue les opérations nécessaires (traitement, redirection, etc.)
Vous noterez que tous les sinon sont gérés automatiquement par le framework de façon transparente (mais ces comportements par défaut sont toujours surchargeables) :
- Par défaut le formulaire présenté sera vierge[2],
- Les champs seront automatiquement préremplis en cas d'erreur de validation,
- Les erreurs seront contextualisées par rapport aux champs.
Rendu du formulaire dans la vue
Enfin, il nous reste à créer un template pour présenter le formulaire et éventuellement afficher un message à l'utilisateur, dans notre vue gérée au travers du fichier apps/main/modules/contact/templates/indexSuccess.php :
<?php if ($sf_user->hasFlash('notice')): ?> <p class="notice"><?php echo $sf_user->getFlash('notice') ?></p> <?php endif; ?> <form action="<?php echo url_for('contact/index') ?>" method="post"> <table> <?php echo $form ?> <tr> <td></td> <td><input type="submit" /></td> </tr> </table> </form>
Si on charge la page /main_dev.php/contact, on a le résultat suivant :

Au passage si vous regardez la source, une protection anti CSRF est automatiquement gérée de façon transparente.
Bien entendu, le mode de rendu par défaut utilise un tableau pour la mise en forme, mais il existe un autre décorateur plus sémantique à base de liste. Pour l'utiliser, modifions notre classe ContactForm :
<?php class ContactForm extends sfForm { public function configure() { // ... $this->widgetSchema->setFormFormatterName('list'); } }
Il conviendra bien entendu d'adapter le template pour enlever les balises tables. On peut aussi imaginer de créer notre propre décorateur HTML. Par exemple, créeons la classe sfWidgetFormSchemaFormatterDiv comme suit :
<?php class sfWidgetFormSchemaFormatterDiv extends sfWidgetFormSchemaFormatter { protected $rowFormat = "<div class=\"form-row\">\n %error%%label%\n %field%%help%\n%hidden_fields%</div>\n", $errorRowFormat = "<div class=\"form-errors\">\n%errors%</div>\n", $helpFormat = '<div class="form-help">%help%</div>', $decoratorFormat = "<div>\n %content%</div>"; }
Encore une fois, modifions notre classe ContactForm pour l'utiliser (après avoir purgé le cache symfony, comme il se doit) :
<?php class ContactForm extends sfForm { public function configure() { // ... $this->widgetSchema->setFormFormatterName('div'); } }
Rendu des widgets
Vous me direz, un simple <?php echo $form ?> n'est pas très satisfaisant du point de vue de l'intégration HTML, qui nécessite bien souvent de prendre la main finement sur le code html généré. Le framework de formulaires de Symfony 1.1 apporte une solution en permettant de générer le rendu de chacun des widgets de façon indépendante
Imaginons par exemple que nous souhaitions gérer spécifiquement la présentation du champ name de notre formulaire ; notre template devient alors :
<?php if ($sf_user->hasFlash('notice')): ?> <p class="notice"><?php echo $sf_user->getFlash('notice') ?></p> <?php endif; ?> <form action="<?php echo url_for('contact/index') ?>" method="post"> <table> <?php echo $form['topic']->renderRow() ?> <tr> <th><?php echo $form['name']->renderLabel() ?></th> <td> <?php echo $form['name']->renderError() ?> <?php echo $form['name']->render(array('class' => 'toto')) ?> </td> </tr> <?php echo $form['email']->renderRow() ?> <?php echo $form['message']->renderRow() ?> <tr> <td></td> <td><input type="submit" /></td> </tr> </table> </form>
Vous noterez qu'on accède ici aux différents widgets du formulaire au moyen de clés de tableaux classiques ; c'est tout simplement car la classe sfForm implémente l'interface ArrayAccess de la SPL. C'est très pratique !
Le nerf de la guerre : la génération de formulaires à partir d'objets Propel
Ayant été à une lointaine époque un fervent adepte de PEAR::FormBuilder, brique permettant de générer des formulaires à partir d'instance d'objets de données ORM, j'étais curieux de voir comment cette fonctionnalité demandée par les développeurs depuis longtemps allait être implémentée dans Symfony 1.1 au travers de l'ORM Propel, bundlé par défaut[3].
Après avoir configuré un accès à notre SGBD préféré[4], nous allons définir une table contact_demand dans notre fichier config/schema.yml pour y stoker nos demandes de contacts :
propel:
contact_demand:
id:
name: { type: varchar, size: 45, required: true }
email: { type: varchar, size: 100, required: true }
topic: { type: varchar, size: 255, required: true }
message: { type: longvarchar, required: true }
created_at:
On lance rapidement la tâche de création de la table et des objets ORM associés :
$ ./symfony propel:build-all $ ./symfony cc
Maintenant, allons faire un tour dans le répertoire lib/model pour voir ce qui a été généré. Surprise, des objets de formulaires ont été automatiquement créés pour nous !
Jetons un oeil plus particulièrement au fichier lib/form/base/BaseContactDemandForm.class.php :
<?php class BaseContactDemandForm extends BaseFormPropel { public function setup() { $this->setWidgets(array( 'id' => new sfWidgetFormInputHidden(), 'name' => new sfWidgetFormInput(), 'email' => new sfWidgetFormInput(), 'topic' => new sfWidgetFormInput(), 'message' => new sfWidgetFormTextarea(), 'created_at' => new sfWidgetFormDateTime(), )); $this->setValidators(array( 'id' => new sfValidatorPropelChoice(array('model' => 'ContactDemand', 'column' => 'Id', 'required' => false)), 'name' => new sfValidatorString(), 'email' => new sfValidatorString(), 'topic' => new sfValidatorString(), 'message' => new sfValidatorString(), 'created_at' => new sfValidatorDateTime(array('required' => false)), )); $this->widgetSchema->setNameFormat('contact_demand[%s]'); $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema); parent::setup(); } public function getModelName() { return 'ContactDemand'; } }
Cela ne vous rappelle rien ? C'est presque quasiment ce que nous avions écrit manuellement précédemment dans notre classe ContactForm. On notera également la présence dans le fichier lib/form/ContactDemandForm.class.php de la classe ContactDemandForm, cette dernière héritant de BaseContactDemandForm.class.php, ce qui nous permettra de surcharger tout ou partie de ses méthodes pour adapter le formulaire généré à nos besoins.
En l'occurrence, il nous faut adapter un peu la méthode configure() pour retrouver notre sélecteur de sujets et réappliquer nos validateurs. Voici le code de la classe ContactDemandForm modifiée en conséquence :
<?php class ContactDemandForm extends BaseContactDemandForm { public function configure() { // Widgets $topics = sfConfig::get('app_contact_topics', array()); $this->setWidgets(array( 'id' => new sfWidgetFormInputHidden(), 'topic' => new sfWidgetFormSelect(array('choices' => $topics)), 'name' => new sfWidgetFormInput(), 'email' => new sfWidgetFormInput(), 'message' => new sfWidgetFormTextarea() )); // Validators $this->setValidators(array( 'id' => new sfValidatorPropelChoice(array('model' => 'ContactDemand', 'column' => 'Id', 'required' => false)), 'topic' => new sfValidatorRegex(array('pattern' => '/[a-z_]/')), 'name' => new sfValidatorString(array('min_length' => 2, 'max_length' => 45)), 'email' => new sfValidatorAnd(array(new sfValidatorEmail(), new sfValidatorString(array('max_length' => 100)))), 'message' => new sfValidatorString(array('min_length' => 10)), 'created_at' => new sfValidatorDateTime(array('required' => false)), )); $this->widgetSchema->setNameFormat('contact_demand[%s]'); $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema); } }
On va maintenant modifier notre action pour prendre en compte notre nouvelle classe de formulaire liée à notre objet Propel :
<?php class contactActions extends sfActions { public function executeIndex(sfWebRequest $request) { $form = new ContactDemandForm(); if ($request->isMethod('post')) // If HTTP method is POST { // Bind submitted values to the contact form instance $form->bind($request->getParameter('contact_demand')); // Validate the form if ($form->isValid()) { // Save submitted values in form's ContactDemand object $form->save(); // Then redirect user to the homepage with a one-shot message $this->getUser()->setFlash('notice', 'Message sent'); $this->redirect('@homepage'); } } // Publish form instance to the view $this->form = $form; } }
Ainsi, un simple $form->save() nous permet de persister en base les données soumises par l'utilisateur au travers du formulaire autogénéré. Un sacré gain de productivité en phase de prototypage, assurement !
En conclusion
Voilà, ce n'est bien évidemment là qu'une infime partie de ce que peut faire le nouveau système de gestion de formulaires de Symfony 1.1, mais c'est un peu à reculons que l'on retourne à l'ancien système 
Notes
[1] Nous ne créons pas de route pour le moment, même si ce serait là une bonne pratique, afin de ne pas surcharger inutilement le contenu de ce tutorial, déjà bien assez dense comme ça 
[2] On aurait bien entendu pu proposer des valeurs par défaut.
[3] Le mécanisme de génération de code devrait permettre de proposer facilement la même fonctionnalité pour Doctrine prochainement.
[4] Je vous laisse le soin de vous référer au tutoriel existant sur ce même blog 
12 commentaires (Ajouter un commentaire)
Merci NiKo ! Bien écrit, comme d'hab'. En effet, la 1.0 sera presque bonne pour la corbeille
Ca donne envie. Mais quid des plugins compatibles ? Existe-t-il une page qui les recense ?
Deuxième question... A l'heure actuelle, pour un nouveau projet, est-ce que ça vaut le coup de partir sur du 1.0.x ? Parce que vu les profonds changements, je me demande si ça en vaut la peine.
Bref, l'évolution ça a du bon mais parfois ça peut vite devenir casse-tête.
> Ca donne envie. Mais quid des plugins compatibles ? Existe-t-il une page qui les recense ?
Pas à ma connaissance. Effectivement, les plugins écris pour la 1.0 seront à reprendre pour la plupart. Mais c'est là que SVN nous aide bien, beaucoup de plugins proposent dès à présent une branche de compatibilité pour la 1.1.
> A l'heure actuelle, pour un nouveau projet, est-ce que ça vaut le coup de partir sur du 1.0.x ?
Je partirai sur la 1.1, ne serait-ce que pour ne pas avoir un code totalement has-been dans un an. Par contre ça va impliquer de se passer d'une documentation exhaustive, ce qui constitue un risque majeur. À pondérer en regard des délais et moyens accordés au projet.
Merci de ta réponse
C'est noté.
Dans le dernier exemple de surcharge du formulaire Propel, je préfère juste surcharger ce que je souhaite plutôt que de redéfinir complètement le formulaire. Et en plus, ça donne quelques exemples supplémentaires :
<php class ContactDemandForm extends BaseContactDemandForm { public function configure() { // suppression du champ created_at (widget et validateur) unset($this->widgetSchema['created_at']); $topics = sfConfig::get('app_contact_topics', array()); // surcharge du widget topic $this->widgetSchema['topic'] = new sfWidgetFormSelect(array('choices' => $topics)); // surcharge des options d'un validateur $this->validatorSchema['name']->setOption('min_length', 2); $this->validatorSchema['name']->setOption('max_length', 45); $this->validatorSchema['message']->setOption('min_length', 10); // ajout d'un validateur en plus de celui par défaut $this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorString(array('max_length' => 100)), )); // modification du validateur topic pour s'assurer qu'on a bien un topic du widget $this->validatorSchema['topic'] = new sfValidatorChoice(array('choices' => array_keys($topics))); } }Hello, merci pour ce nouveau tuto sur Symfony.

Je commence tout juste à m'y mettre grâce à tes posts
Petite question d'un newbie, j'accède bien au formulaire via http://monvirtualhost/contact
mais pas comme indiqué dans le tuto via http://monvirtualhost/frontend_dev.... (erreur 404).
Est-ce normal ?
Encore une petite chose qui pourrait être utile pour les 100% débutants comme moi, j'ai mis pas mal de temps à trouver ou modifier la template (c'est pourtant plutôt intuitif), ce serait peut-être cool de le rappeler dans le tuto (apps/main/modules/contact/templates/indexSuccess.php).
Enfin en même temps il va bien falloir que je me plonge dans la doc symfony
En tout cas un grand merci à toi.
Et vivement la suite !
Saturn> Haha, la typo qui tue... C'est par
/main_dev.php/contactqu'il faut accéder à la page, je corrige ça dans le billet de suiteFabien> Wow effectivement, c'est plus clair comme ça
Vivement la doc \o/
Salut,
merci beaucoup pour ces articles, c'est très intéressant même pour les gens qui ne touchent pas à PHP comme c'est mon cas...
Au boulot, j'utilise Java et AppFuse (une sorte de projet de base regroupant tous les outils et frameworks dont on a besoin), et c'est très ressemblant à ce que tu décris avec Symfony ; du coup, je ne suis pas trop perdu lors de la lecture
Un jour, je me remettrai à PHP... peut-être.
Pierre> Oui, AppFuse a très bonne presse côté Java, et Grails semble avoir de bons retours même si y'a un effet hype un peu désagréable.
En tout cas, peu importe le flacon pourvu qu'on ait l'ivresse
fabien > Il faut faire unset($this['created_at']) pour supprimer un element du formulaire. ($this est le formulaire).
Dans le cas d'un sfFormPropel, si on supprime seulement le widget alors cela pose un bug dans le sfValidatorSchema. En effet la propriete sfValidatorSchema::fields contient toujours le champs 'created_at', et lors du 'clean' la valeur sera mise a NULL.
Cela peut etre tres embetant si ce champ est un 'id' par exemple.
Bonjour
Merci pour ce tuto très intéressant. J'en profite pour vous demander si vous avez déjà tenter de mettre un sfWidgetFormInputFile dans un formulaire pour faire un upload de fichier.
public function configure() { $this->widgetSchema['file_attachment'] = new sfWidgetFormInputFile(); $this->validatorSchema['body']->setOption('required',true); $this->validatorSchema['file_attachment'] = new sfValidatorFile( ); $this->validatorSchema['file_attachment']->setOption('mime_types', array('application/pdf')); }Le code ci-dessus entraine systématiquement une validation refusée avec comme message de retour "REQUIRED". Même problème si je met l'option required à false pour le validator de file_att...
Une idée ???
Olivier BALAIS
Olivier> Non, je n'ai pas encore joué avec ce widget, mais n'hésite pas à poser la question sur la mailing-list de dev ou à ouvrir un ticket