Cibul Tech Blog - Fiddling with PHP, javascript and things

Archive for ‘Uncategorized @fr’

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:

  1. Mise en place du projet: nous allons créer un projet Symfony et une base de données mysql
  2. 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
  3. Ajout de nouveaux champs occurrences: nous allons ajouter la possibilité d’ajouter des occurrences en utilisant jQuery fonctions Ajax
  4. Suppression de nouveaux champs occurrences: toujours avec jQuery, nous allons permettre à l’utilisateur de supprimer des champs occurrences vierges
  5. 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:

#config/doctrine/schema.yml
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:

#data/fixtures/fixtures.yml
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

<?php // apps/frontend/modules/event/actions/actions.class.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.

#app/frontend/config/routing.yml
homepage
:
  url
:  /
  param
: { module: event, action: index }

Les occurrences sont listées sous chaque événement dans le modèle indexSuccess.php

<!--apps/frontend/modules/templates/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
  • // apps/frontend/modules/event/actions/actions.class.php

        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);
        }
  • Le template editSuccess: Il définit le formulaire et le prepare pour l’envoi du resultat a l’action
  • submit
  • <!--apps/frontend/modules/templates/editSuccess.php-->
    <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>
  • Le routing doit etre mis a jour avec les routes edit and submit:
  • #app/frontend/config/routing.yml
    edit
    :
      url
    : /edit
      param
    : { module: event, action: edit }

    submit
    :
      url
    : /submit
      param
    : { module: event, action: 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 ».
  • // apps/frontend/modules/event/actions/actions.class.php

        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');
        }
  • 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:
  • <!--apps/frontend/modules/templates/indexSuccess.php -->

    <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:

    <?php //lib/form/doctrine/EventForm.class.php

    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:

    <?php //lib/form/doctrine/OccurrenceForm.class.php

    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:

    <!--apps/frontend/modules/templates/editSuccess.php-->
    <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:

    <!--app/frontend/template/layout.php-->
    ...
        <?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.

    <!--apps/frontend/modules/templates/editSuccess.php-->
    ...
    </ul>
    <a id="addoccurrence" href="#">Add an occurrence</a>

    Dans le fichier javascript (eventform.js), ajoutez ca:

    /* web/js/eventform.js */
    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…

    #app/frontend/config/routing.yml
    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.

    // apps/frontend/modules/event/actions/actions.class.php

        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.

    </p>
    //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é).

    //apps/frontend/modules/event/templates/_addNew.php
    <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.

    //lib/form/doctrine/EventForm.class.php

      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

    <!--apps/frontend/modules/event/templates/_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é.

    /* web/js/eventform.js */

    var removeNew = function(){
      $('.removenew').click(function(e){
        e.preventDefault();
        $(this).parent().remove();
      })
    };

    Toujours dans le fichier JavaScript, mettez à jour le comportement du bouton #addoccurrence:

    /* web/js/eventform.js */

    $(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):

    <!--apps/frontend/modules/templates/indexSuccess.php -->

    <a href="<?php echo url_for('@new')?>">Add an Event</a>

    Mettons à jour le routing.

    #app/frontend/config/routing.yml
    new
    :
      url
    : /new
      param
    : { module: event, action: new }

    … et creons l’action executeNew

        public function executeNew(sfWebRequest $request){
          $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

    <!--apps/frontend/modules/templates/editSuccess.php-->
        <?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 | 17 Comments »

Cibul Tech: des bidouilles en PHP

Friday, April 16th, 2010

Salut, soyez les bienvenus sur le blog Cibul Tech! Le but de ce blog sera de publier des articles sur ce qui nous semble intéressants de mentionner alors que nous avançons en trébuchant dans le développement d’une application web. Personnellement, je suis relativement nouveau dans le développement PHP, javascript.. et je n’ai commencé que récemment à jouer avec le framework Symfony, et comme je lutte avec les bouts de code, la documentation, les tutoriels, les conseils de blogs et autres ressources en ligne me sont très utiles. Ce blog sera ma tentative de contribuer à mon tour à la communauté PHP.

Quoi qu’il en soit, j’espère que vous trouverez quelques-uns des articles affichés ici utiles, tout commentaire sera grandement apprécié!

Posted in Uncategorized @fr | 1 Comment »