Formulaires intégrés avec Symfony 1.4 et jQuery
Friday, April 16th, 2010
Comment mettre en place une interface unique pour insérer plusieurs entrées liées par des relations de un à plusieurs…
J’ai cherché récemment un moyen de construire un formulaire qui permettrait la saisie de données (édition ou ajout de nouvelles données) en de multiples objets liés. Il s’avère que la façon du faire dans Symfony est d’utiliser des formulaires intégrées. Je ne suis un expert ni en Symfony, ni dans jQuery, aussi si il existe une meilleure méthode que celle décrite ici, je serai heureux de lire vos commentaires.
Nous allons construire une liste d’événements modifiable par l’utilisateur. Chaque événement a une ou plusieurs occurrences, définies par une date et un lieu. L’interface doit permettre à l’utilisateur de modifier chaque événement et ses occurrences, et également d’ajouter des événements et des occurrences.
Nous ferons cela en 5 étapes:
- Mise en place du projet: nous allons créer un projet Symfony et une base de données mysql
- Edition des événements: nous allons créer un formulaire dans lequel événements et occurrences peuvent être édités en utilisant les formulaires intégrés
- Ajout de nouveaux champs occurrences: nous allons ajouter la possibilité d’ajouter des occurrences en utilisant jQuery fonctions Ajax
- Suppression de nouveaux champs occurrences: toujours avec jQuery, nous allons permettre à l’utilisateur de supprimer des champs occurrences vierges
- Création d’un nouvel événement: Nous allons légèrement modifier le modèle de formulaire pour permettre à l’utilisateur d’ajouter d’autres événements à la liste
1. Mise en place du projet et affichage de la page d’index
Nous commençons par la mise en place d’un projet Symfony, d’une base de données basée sur le schéma ci-dessous et de fixtures:
Le schéma:
Event:
columns:
id:
type: integer
primary: true
autoincrement: true
title:
type: string(255)
notnull: true
relations:
Occurrences:
type: many
class: Occurrence
local: id
foreign: event_id
onDelete: CASCADE
Occurrence:
columns:
id:
type: integer
primary: true
autoincrement: true
location:
type: string(255)
notnull: true
date:
type: date
notnull: true
event_id:
type: integer
notnull: true
relations:
Event:
type: one
local: event_id
… et les fixtures:
Event:
apero_geant:
title: Apero Geant
rugby_night:
title: Rugby Night
Occurrence:
apero_geant1:
Event: apero_geant
location: Marseille, France
date: '2009-08-29'
apero_geant2:
Event: apero_geant
location: Nantes, France
date: '2009-11-10'
apero_geant3:
Event: apero_geant
location: Rennes, France
date: '2010-03-25'
rugby_night1:
Event: rugby_night
location: Yvetot, France
date: '2010-03-20'
Après avoir généré une application frontend, nous pouvons créer un module «événement» en ajoutant simplement les répertoires event/actions et event/templates sous le répertoire des modules de l’application.
Dans un fichier action.class.php, nous allons créer un premier index action qui envoie une liste des événements à un template indexSuccess.php
class eventActions extends sfActions{
public function executeIndex(sfWebRequest $request){
$this->events = Doctrine_Core::getTable('Event')->findAll();
}
}
Pour faire simple, nous allons définir l’action index par défaut dans le routing, en supprimant le contenu par défaut.
homepage:
url: /
param: { module: event, action: index }
Les occurrences sont listées sous chaque événement dans le modèle indexSuccess.php
<h2>Events Index</h2>
<?php foreach($events as $event): ?>
<div class="event-item">
<h3><?php echo $event->getTitle() ?></h3>
<ul>
<?php foreach($event->getOccurrences() as $occurrence): ?>
<li><?php echo $occurrence->getDate()?> - <?php echo $occurrence->getLocation() ?></li>
<?php endforeach;?>
</ul>
</div>
<?php endforeach; ?>
Voila, le premier pas est fait et le projet est mis en place avec une page d’index par défaut indiquant les événements et leurs occurrences.

2 – Modification des événements existants et de leurs occurrences
Maintenant, nous devons mettre en place la structure du formulaire intégré.
Nous voulons pouvoir modifier les événements et leurs occurrences dans le même formulaire. Commençons par créer un edit/action et son modèle associé puis lions celui-ci à la page d’index. Nous allons commencer simplement en creant un formulaire standard qui nous permettra de mettre à jour le champ titre de l’événement. Une fois que celui-ci fonctionnera, nous commencerons à travailler avec les formulaires intégrées pour ajouter la possibilité de modifier les occurrences liées.
Pour que cela fonctionne, nous devons créer une action edit ainsi que son modèle pour lancer le formulaire d’édition, une action de validation pour mettre à jour la base de données quand le formulaire a été rempli et nous allons ajouter des liens vers la page d’index pour pouvoir accéder au menu d’édition pour chaque événement
- Commençons par l’action executeEdit: elle crée simplement un formulaire et l’envoie au template editSuccess.php
- Le template editSuccess: Il définit le formulaire et le prepare pour l’envoi du resultat a l’action
- submit
- Le routing doit etre mis a jour avec les routes edit and submit:
- Et l’action de validation executeSubmit(): elle permet d’extraire de la requête les valeurs transmises par le formulaire, récupère l’objet événement sur la base des id extraits, génère un formulaire, le lie avec les données de la requête et met à jour la base de données. Il est important ici de se concentrer sur ce qu’est la liaison (bind) d’un formulaire: en principe, lorsque le formulaire est créé à partir d’un objet événement généré depuis la base de données, les valeurs stockées sont celles de la base de données et non pas celles entrées par l’utilisateur dans la page précédente, qui sont encore stockées dans les parametres de la requête. La liaison (bind) consiste à prendre les valeurs de la requête et de les utiliser pour mettre à jour le formulaire nouvellement créé, en vérifiant la validité des données dans le processus, selon les règles du formulaire. Si la validité est confirmée, la base de données est mise à jour par la méthode « save ».
- L’index devrait avoir un lien pour chaque événement afin de permettre aux utilisateurs d’afficher la page d’édition, aussi ajoutez ceci sous le titre de l’événement:
public function executeEdit(sfWebRequest $request){
$this->forward404Unless($event = Doctrine::getTable('Event')->find(array($request->getParameter('id'))), sprintf('Event does not exist (%s).', $request->getParameter('id')));
$this->form = new EventForm($event);
}
<h2>Edit Event</h2>
<form action="<?php echo url_for('@submit') ?>" method="post">
<?php echo $form->renderHiddenFields() ?>
<?php echo $form['title']->renderLabel()?> <?php echo $form['title']->renderError()?> <?php echo $form['title'] ?>
<input type="submit" value="Save" />
</form>
<a href="<?php echo url_for('@homepage')?>">Back to index</a>
edit:
url: /edit
param: { module: event, action: edit }
submit:
url: /submit
param: { module: event, action: submit }
public function executeSubmit(sfWebRequest $request){
$tainted_values = $request->getParameter('event');
$event = Doctrine::getTable('Event')->find($tainted_values['id']);
$this->form = new EventForm($event);
if ($request->isMethod('post') && $this->form->bindAndSave($tainted_values))
$this->redirect('@homepage');
$this->setTemplate('edit');
}
<a href="<?php echo url_for('event/edit?id='.$event->getId())?>">edit event</a>

Maintenant vous devriez avoir un moyen simple de mettre à jour les titres événement. Mais voici la partie difficile: comment mettre à jour les données des occurrences de l’événement? Cela peut effectivement se faire facilement en jouant avec le formulaire d’événement et intégration du formulaire d’occurrence. Mais nous ne voulons intégrer uniquement les occurrences liées à l’événement. Une méthode sert à cette fin et peut être utilisée pour faire exactement cela en une ligne: embedRelation(). Cette méthode prend en gros tous les objets liés et génère les formulaires intégrés correspondants. Assez bla bla, mettons à jour la classe EventForm:
class EventForm extends BaseEventForm
{
public function configure()
{
$this->embedRelation('Occurrences');
}
}
Ouvrez le OccurrenceForm.class.php et faites un unset sur les variables qui devraient être ignorées par le formulaire:
class OccurrenceForm extends BaseOccurrenceForm
{
public function configure()
{
unset($this['event_id']);
}
}
Mettons maintenant à jour le modèle editSuccess.php pour afficher les occurrences d’un événement quand elles existent … Ca va sous le champ titre de l’événement:
<ul>
<?php foreach ($form['Occurrences'] as $occurrence):?>
<li>
<?php echo $occurrence['date']->renderLabel() ?> <?php echo $occurrence['date']->renderError() ?>
<?php echo $occurrence['date'] ?>
-
<?php echo $occurrence['location']->renderLabel() ?> <?php echo $occurrence['location']->renderError() ?>
<?php echo $occurrence['location'] ?>
</li>
<?php endforeach ?>
</ul>
C’est fait. On peux modifier les occurrences dans le meme formulaire.

3 – Ajout de nouveaux champs d’occurrences avec jQuery
Et si on veux ajouter de nouvelles occurrences à un événement? Ca ne serait pas sympa d’avoir un moyen propre pour ajouter de nouveaux champs d’occurrences à volonté?
Bien sûr que oui. Et on va y arriver en utilisant des fonctions Ajax avec jQuery.
Nous allons ajouter un peu de javascript sur la page d’édition qui consistera à interroger le serveur pour certains éléments du formulaire, et une fois reçus, les ajouter au reste de la page sans avoir besoin d’un rafraîchissement. Du côté du serveur, une nouvelle action se chargera de la requête Ajax et réassemblera les formulaires pour balancer le bon élément.
Nous commençons par ajouter la bibliothèque jQuery (j’utilise jQuery 1.4.2) au dossier JS ainsi qu’un fichier js vide (eventform.js). Dans le layout.php de l’application frontale, on ajoute une référence à chaque fichier de l’en-tête:
...
<?php include_stylesheets() ?>
<?php use_javascript('jquery-1.4.2.min.js') ?>
<?php use_javascript('eventform.js') ?>
<?php include_javascripts() ?>
...
Dans le modèle editSuccess.php, nous allons ajouter un bouton (un lien en fait) après la liste d’occurrences. Nous le lierons au comportement javascript pour ajouter les nouveaux champs.
...
</ul>
<a id="addoccurrence" href="#">Add an occurrence</a>
Dans le fichier javascript (eventform.js), ajoutez ca:
newfieldscount = 0;
function addNewField(num){
return $.ajax({
type: 'GET',
url: '/add?num='+num,
async: false
}).responseText;
}
$(document).ready(function(){
$('#addoccurrence').click(function(e){
e.preventDefault();
$('ul').append(addNewField(newfieldscount));
newfieldscount = newfieldscount + 1;
});
});
Chaque fois que l’élément de page ayant pour ID #addoccurrence sera cliqué, un nouveau champ sera récupéré par une requête Ajax et sera ajouté à la liste d’occurrences. La pièce manquante est maintenant le traitement de la requête par le serveur. La première chose est de mettre à jour le routing en ajoutant la route que nous décrivons dans le fichier javascript…
add:
url: /add
param: { module: event, action: add }
… Et d’ajouter l’action correspondante, qui devrait fonctionner uniquement lorsque la requête provient d’un XMLHttpRequest.
public function executeAdd(sfWebRequest $request){
$this->forward404unless($request->isXmlHttpRequest());
$number = intval($request->getParameter("num"));
$this->form = new EventForm();
$this->form->addNewFields($number);
return $this->renderPartial('addNew',array('form' => $this->form, 'number' => $number));
}
L’action appelle une méthode de la classe EventForm qui ajoutera le nombre d’occurrences nécessaires pour reproduire la structure du formulaire affichée.
//lib/form/doctrine/EventForm.class.php
public function addNewFields($number){
$new_occurrences = new BaseForm();
for($i=0; $i <= $number; $i+=1){
$occurrence = new Occurrence();
$occurrence->setEvent($this->getObject());
$occurrence_form = new OccurrenceForm($occurrence);
$new_occurrences->embedForm($i,$occurrence_form);
}
$this->embedForm('new', $new_occurrences);
}
Comme vous pouvez le voir, les formulaires se comportent comme des tableaux. Lorsque vous avez besoin pour créer un formulaire avec plusieurs formulaires intégrés, vous devez d’abord créer un tableau du formulaire et y intégrez tous les formulaires dont vous avez besoin. Une fois que vous avez terminé, vous intégrez ce tableau dans votre formulaire principal.
Maintenant nous avons besoin de créer la partie qui contient le code HTML à renvoyer à l’appel Ajax. Il ne doit contenir que le dernier formulaire intégré (celui qui doit être ajouté).
<li>
<?php echo $form['new'][$number]['date']->renderLabel() ?> <?php echo $form['new'][$number]['date']->renderError() ?>
<?php echo $form['new'][$number]['date'] ?>
-
<?php echo $form['new'][$number]['location']->renderLabel() ?> <?php echo $form['new'][$number]['location']->renderError() ?>
<?php echo $form['new'][$number]['location'] ?>
</li>
Une dernière chose: maintenant, quand le formulaire événement est validé et reçu par l’action de validation, l’action crée un formulaire auquel les données dans la requête sera liées (les données que vous venez de soumettre). Mais avant que cela puisse arriver, la structure du formulaire doit correspondre aux données reçues: le nombre de nouveaux formulaires intégrés doit être le même que le nombre validé. La méthode « bind » dans le EventForm fait exactement cela: il recrée la structure de la requête dans le formulaire créé avant que la liaison (bind) ne soit faite.
public function bind(array $taintedValues = null, array $taintedFiles = null){
$new_occurrences = new BaseForm();
foreach($taintedValues['new'] as $key => $new_occurrence){
$occurrence = new Occurrence();
$occurrence->setEvent($this->getObject());
$occurrence_form = new OccurrenceForm($occurrence);
$new_occurrences->embedForm($key,$occurrence_form);
}
$this->embedForm('new',$new_occurrences);
parent::bind($taintedValues, $taintedFiles);
}
4 – Retirer les nouveaux champs d’occurrences avec jQuery
Et si on retirait les champs d’occurrences laissés vides? C’est moins compliqué car ca ne nécessite aucun traitement côté serveur. Tout ce que nous devons faire maintenant est d’ajouter un lien pour supprimer chacun des champs d’occurrences et de leur attribuer un comportement js.
Le lien « remove » doit être ajouté dans le partial addNew.php
...
<a class="removenew" href="#">Remove</a>
</li>
Ici, un comportement doit y être lié. Une petite chose: comme ce lien est chargé par javascript, le comportement doit être chargé après que l’élément ait été chargé.
Nous devons mettre à jour les eventform.js avec une fonction removeNew qui sera lancée à chaque fois que le lien « remove » est cliqué.
var removeNew = function(){
$('.removenew').click(function(e){
e.preventDefault();
$(this).parent().remove();
})
};
Toujours dans le fichier JavaScript, mettez à jour le comportement du bouton #addoccurrence:
$(document).ready(function(){
$('#addoccurrence').click(function(e){
e.preventDefault();
$('ul').append(addNewField(newfieldscount));
newfieldscount = newfieldscount + 1;
$('.removenew').unbind('click');
removeNew();
});
});
Et Voila.
5 – Ajout de nouveaux événements
Une dernière chosea faire: ajouter un lien vers la page d’index pour afficher un formulaire d’ajout de nouveaux événements. Quand un nouvel événement est créé, il doit contenir au moins une occurrence. Cela devrait être simple: nous allons réutiliser le modèle indexSuccess.php en le modifiant légèrement et ajouter une nouvelle action.
Nous pouvons créer le lien dans la page d’index (à la fin):
<a href="<?php echo url_for('@new')?>">Add an Event</a>
Mettons à jour le routing.
new:
url: /new
param: { module: event, action: new }
… et creons l’action executeNew
$event = new Event();
$this->form = new EventForm($event);
$this->form->addNewFields(0);
$this->setTemplate('edit');
}
… Et mettez également à jour le modèle editSuccess.php afin qu’ un nouveau champ soit déjà disponible (et non amovible) lorsque le formulaire est pour un nouvel événement. Ajoutez juste après la balise ul
<?php if ($form->getObject()->isNew()): ?>
<script type="text/javascript">newfieldscount = 1;</script>
<li>
<?php echo $form['new'][0]['date']->renderLabel() ?> <?php echo $form['new'][0]['date']->renderError() ?>
<?php echo $form['new'][0]['date'] ?>
-
<?php echo $form['new'][0]['location']->renderLabel() ?> <?php echo $form['new'][0]['location']->renderError() ?>
<?php echo $form['new'][0]['location'] ?>
</li>
<?php endif ?>
Je vous laisse faire la même chose avec le titre pour remplacer “Modifier l’événement” par “Nouvel événement”

Nous y sommes arrivés! Cela devrait vous permettre de modifier et d’ajouter des événements et leurs occurrences, en utilisant les formulaires intégrés et les fonctions dynamiques pour ajouter d’autres occurrences. J’espère que ce sera utile pour vous. Encore une fois, vos suggestions et commentaires sont les bienvenus.
Au fait, vous pouvez télécharger le code ici.

Merci a Nacho et Nicolas, leur post m’ont ete tres utiles. Il y a un autre tutoriel interressant sur les formulaires imbriques ici.
Posted in Uncategorized @fr | 16 Comments »