Au départ de ce projet, le sentiment de se poser souvent les même questions pour réaliser une application web de consultation/mise à jour de données issues d’une base de données. Quelle bibliothèque de composants utiliser, comment gérer mes formulaires, comment utiliser des tableaux évolués, comment accéder aux données, …

sfForm_to_extJSCe genre d’application regroupe principalement les fonctionnalités suivantes :

  • grille avec pagination pour lister les enregistrements des différentes tables,
  • filtrage de ces grilles,
  • formulaire d’édition des enregistrements,
  • imbrication « complexe » de formulaire (e.g. : édition d’un auteur avec ses articles en même temps),
  • validation par rechargement des pages (et non pas par Ajax, afin de rassurer l’utilisateur, ce qu’il voit sur la page correspond bien à ce qui est en base).

La réflexion

Symfony s’impose assez facilement comme base de développement. Il possède une gestion de routing très puissante, une DAO intégrée (Doctrine) , une gestion de formulaire complète (un élément important dans le choix de notre solution), une structure permettant le développement en parallèle sans gêne.

ExtJs est une bibliothèque javascript évoluée pour faire des applications riches. Elle possèdent des composants de haut niveau (grille, arbre, combo, …), un système d’évènement complet, différents layout offrant toutes les possibilités de mise en forme, une abstraction de l’accès au données (via les stores) …

Le but est donc de mettre en commun ces deux environnements.

Schema-EXTJS-Symfony-1

Explication des choix

Les autres plugins existants

Il existe un plugin pour Symfony 1.0 et ExtJs 2.1 (sfExtjs2Plugin)

Ce plugin a pour principe de créer le code ExtJs complètement depuis symfony via un helper et des méthodes « asMethod, asVariable, … ». Au lieu d’écrire du code javascript, on écrit du php qui lui va écrire du javascript.

Ce principe est intéressant pour certaines applications, il permet de centraliser tout le code à un seul endroit.

Mais ce plugin n’est pas adapté au formulaire de symfony (version 1.2). Or c’est un des avantages que l’on voulait utiliser. Donc l’utilisation de ce plugin était dès le départ compromise.

Formulaire vers Widgets

Le besoin initial portait sur les formulaires, mais cela pouvait aussi s’appliquer à d’autres composants ExtJS, tels les tableaux, les fieldset, les panel, …

En construisant la page comme une arborescence de composants ExtJS, on peut décrire côté symfony la page comme une arborescence de widgets (panel contenant un fieldset contenant une grille), et on peut garder aussi le découplage formulaire/template.

Description de la solution

ExtJs 2 définissait un « xtype » pour quasiment tous les « composants » graphique, ExtJs 3 a poussé le concept en définissant un « xtype » sur tous les classes (store, colonne, …). Cela permet d’écrire une page complète uniquement via une description JSON.

C’est donc sur cette méthode que nous nous sommes orientés.

Une classe de base « PluginExt3Base » qui étend « sfWidgetForm » permet de gérer les attributs d’un objet, des gérer les sous-éléments, et possède des fonctions pour convertir cet objet en une description JSON.

D’autres classes, qui héritent de celle-ci, plus spécifiques, permettent une définition directe d’un type d’objet (« PluginExt3Label », « PluginExt3Panel », …) et possèdent des fonctions propre à ce type d’objet.

Un Helper a été mis en place pour automatiser l’écriture des objets dans les variables adéquates. Pour interpréter la description JSON, un fichier javascript récupère les variables écrite par le template Symfony, et crée les objets grâce à la fonction Ext.ComponentMgr.create.

Architecture de l'intégration de Symfony et ExtJs

Pour les formulaires

Symfony utilise un système de routing très performant et très simple en même temps. Une URL correspond à une action d’un module. Une URL est définie par son adresse mais aussi par sa méthode. Un appel en GET ou en POST peut donc être redirigé vers la même action du même module, ou bien vers 2 actions distinctes.

Les navigateurs ne permettent pas tous de gérer les 4 méthodes de HTTP (GET, POST, DELETE et PUT) donc Symfony a mis en place un système pour gérer cela. Dans une requête, si un paramètre « sf_method » est passé, alors la valeur de ce paramètre permet de définir la méthode de l’appel.

Lors de la création de l’objet spécifique formulaire, on passe en paramètre l’URL de destination ainsi que la méthode d’appel. Cela permet de gérer tous les cas possible.

Les formulaires Symfony sont composés de 2 parties, une définition des widgets (pour 1 champs, comment il est représenté) et une définition des validators (pour 1 champs, quelles sont les règles qu’il possède)

Les widgets de base de Symfony sont remplacé par les widgets ExtJs, et les validators sont gardés. Cela permet de garder toutes la puissances de traitement automatique des formulaires (validation, message d’erreur, …) tout en ayant la flexibilité de l’affichage ExtJs.

Pour d’autres pages

Dans les templates, l’organisation de la page est libre. Il est relativement facile de créer un panel qui contient 2 panels qui contiennent chacun des onglets qui eux mêmes contiennent chacun une grille …

Les templates étant des fichiers php, il est tout à fait possible d’automatiser la création de composants dans une boucle, de conditionner l’affichage, etc. Ceci autorise la création de pages relativement chargées et complexes avec très peu de code.

Quand un composant, ou un comportement est vraiment trop complexe, il est tout a fait possible de créer un nouveau composant ExtJs dans un fichier javascript, de lui attribuer un « xtype », et de créer le composant correspondant dans l’application.

Pour une application, nous avons ainsi développé un composant qui hérite d’une grille, et qui le complète avec une gestion automatique de la pagination, des filtres par colonne, des liens par ligne, et d’autres possibilités.

Une fois ce composant créé, il suffit, dans un template, d’instancier cet élément pour avoir accès à toutes ses fonctionnalités. Et comme ExtJs permet facilement de faire de l’héritage, il est encore plus facile de créer un composant possédant un comportement bien spécifique.

Exemple de code :

Définition d’un formulaire. Celui-ci permet de faire le lien entre un objet et une table de la base de donnée, de définir sous quel format apparaitrons les champs et de définir quels sont les contrôles qui seront appliqués.

lib/form/doctrineExt/usersExt3Form.php

class usersExt3Form extends PluginExt3FormDoctrine
{
  public function setup()
  {
    parent::setup();
 
    // create combobox "type user"
    $result = Doctrine::getTable('typeusers')->getAll()->execute();
    $cboUserType = PluginExt3DoctrineComboBox::fromResult($result, 'idtypeuser', 'labeltypeuser');
    $cboUserType->setAttribute('fieldLabel', 'User Type');
    $cboUserType->setAttribute('allowBlank', false);
 
    $widgets = array (
    'lname'=> new PluginExt3TextField( array ('allowBlank'=>false, 'fieldLabel'=>'Last name')),
    'fname'=> new PluginExt3TextField( array ('allowBlank'=>false, 'fieldLabel'=>'First name')),
    'codeidentification'=> new PluginExt3TextField(array('fieldLabel'=>'Identification code')),
    'login'=> new PluginExt3TextField( array ('allowBlank'=>false)),
    'email'=> new PluginExt3EmailField(),
    'idusertype'=>$cboUserType
    );
    $this->setWidgets($widgets);
 
    $validators = array (
    'lname'=>$this->getWidget('lname')->getValidator(),
    'fname'=>$this->getWidget('fname')->getValidator(),
    'codeidentification'=>$this->getWidget('codeidentification')->getValidator(),
    'login'=> $this->getWidget('login')->getValidator(),
    'email'=>$this->getWidget('email')->getValidator(),
    'idusertype'=>$this->getWidget('idusertype')->getValidator()
    );
    $this->setValidators($validators);
 
    $this->widgetSchema->setNameFormat('users[%s]');
  }
 
  public function getModelName()
  {
    return 'users';
  }
}

Le fichier d’action qui permet de créer le formulaire, de le remplir si on est en méthode « POST », de contrôler le formulaire, et enfin de sauvegarde l’objet lié au formualire

actions/actions.class.php

class sfMonAppActions extends sfActions
{
  public function executeIndex(sfWebRequest $request)
  {
    $this->form = new usersExt3Form();
 
    if ($request->isMethod('POST'))
    {
      $this->form->bind($request->getParameter($this->form->getName()));
      if ($this->form->isValid())
      {
        $this->form->save();
        $this->getUser()->setMessage('OK', 'successful');
        $this->redirect('@home');
      }
      else
      {
        $this->getUser()->setMessage('ERROR', 'Forms contains error', $this->form->getErrorSchema());
      }
    }
  }
}

Le template permet de définir l’affichage final.
templates/indexSuccess.php

$formPanel = new PluginExt3FormPanel('/', $form);
  $formPanel->setAttribute('title', "Hello World !");
  $ret = '<script type="text/javascript">// <![CDATA[
mce:0
// ]]></script>'; echo $ret;

Côté client un fichier javascript permet d’interpréter le code final généré par le template

Quelques points précis

Validators

Les validators de Symfony sont utilisés pour contrôler les formulaires. Il existe de nombreux validators (date, e-mail, url, texte, …) et il est facile de créer de nouveaux validators, ou d’étendre un validator existant pour le spécialiser.

Côté ExtJs, des validators « simple » peuvent être utiliser pour contrôler la saisie (champs obligatoire, champ numérique).

La combinaison de ces deux types de contrôles permet d’avoir une interface riche, interactive et de minimiser les échanges avec le serveur.

Exemple :
Définition d’un formulaire, avec des propriétés de contrôle :

$widgets = array(
 'name' =&gt; new PluginExt3TextField(array('fieldLabel'=&gt;'Nom')),
 'code' =&gt; new PluginExt3TextField(array('maxLength'=&gt;5, 'minLength' =&gt;5, '@maskRe' =&gt;'/^[0-9]$/', 'allowBlank'=&gt;false)),
 'startdate' =&gt; new PluginExt3DateField(array('fieldLabel'=&gt;'Date de début'))
);

Code JSON généré :

{
  xtype:"textfield",
  autoCreate: {tag:"input",type:"text",size:20,autocomplete:"on"},
  name:"mydata[name]",
  value:null,
  fieldLabel:"Nom",
  invalidText:""
},{
  xtype:"textfield",
  maxLength:5,
  minLength:5,
  maskRe:/^[0-9]$/,
  autoCreate: {tag:"input",type:"text",size:20,autocomplete:"on"},
  name:"mydata[code]",
  value:null,
  fieldLabel:"Code",
  invalidText:""
},{
  xtype:"datefield",
  format:"m/d/Y",
  invalid:"Invalid date format : mm/dd/yyyy",
  altFormats:"d/m/Y|d/m/y|Y-m-d",
  allowBlank:true,
  fieldLabel:"Start date",
  autoCreate: {tag:"input",type:"text",size:20,autocomplete:"on"},
  name:"mydata[startdate]",
  value:null,
  invalidText:""
}

Visuel rendu :

Rendu visuel

Imbrication de sous-formulaires

De base, les formulaires Symfony peuvent « inclure » d’autres formulaires, et utilisent un « décorateur » pour l’inclusion.

Symfony propose de lier un formulaire à un objet (de la DAO ou autre). Cela permet de d’avoir directement au niveau du formulaire des méthodes pour mettre à jour l’objet en question et pour le sauvegarder. Cela implique d’avoir au niveau des formulaires les méthodes nécessaires. Cela permet entre autre une gestion des formulaires imbriqués (mise à jour et sauvegarde en cascade).

Pour notre application, cet aspect des formulaires est important, nos formulaires héritent donc de « sfFormDoctrine ».

Le choix de construire toute l’arborescence de la page par une hiérarchie de composants nous oblige à imbriquer nos formulaires dans des widgets qui n’ont pas une vocation de formulaire (filedset, …).

Nous avons donc développé un objet « dummyExt3FormDoctrine » qui permet de faire « comme si » le formulaire était basé sur un objet, alors que ce n’est pas le cas. Cela nous permet de garder les avantages des formulaires Doctrine avec une gestion en cascade des sous-formulaires.

Cet objet autorise donc la création de formulaire contenant des sous-formulaires avec un décorateur spécifique (fieldset, colonne, …), mais permet aussi, grâce à l’héritage, de faire des formulaires dont une partie est lié à la base de données, les ‘autres parties possédant uniquement du code métier. Toutes les imbrications sont possibles.

Et MVC dans tout ça ?

Avec cette technique, on perd un peu le découplage Vue /Contrôleur car le contrôleur va lui-même créer les formulaires et les mettre en forme.

Le modèle gère complètement tout l’accès à la base de donnée, et est relativement « indépendant » de la vue et du contrôleur. Une majorité du code métier est inclus dans cette partie (création de données dans les tables annexes par exemple).

Le contrôleur fait peu de chose. Il instancie les formulaires, appelle les fonctions sur ces formulaires (isValid, save, …), s’occupe de récupérer les données de la DAO pour les proposer à la vue. Il possède peu de code métier.

La vue s’occupe de toute la mise en forme, elle s’occupe de créer les grilles, de rendre les formulaires.

Le découplage effectué est relativement respectueux du processus MVC.

Le seul point non respectueux se produit lors de l’imbrication des formulaires par le contrôleur car c’est ce contrôleur qui va définir le décorateur à utiliser, et non la vue.

Page officiel du plugins : http://www.symfony-project.org/plugins/atolExt3WidgetPlugin
Symfony : http://www.symfony-project.org/
ExtJs : http://www.extjs.com/