[Admin generator] Paramètre ‘table method’

Une petite astuce que j’utilise depuis longtemps mais que je n’avais jamais pensé à partager : optimiser le nombre de requêtes Doctrine et par la même occasion, un module d’admin generator.

J’ai un module d’admin generator article lié au modèle Article.

  • schema.yml
Article:
  connection: doctrine
  actAs:
    Timestampable: ~
    Taggable: ~
    Sluggable:
      fields: [titre]
      name: slug
      type: string
      length: 255
      unique: true
      canUpdate: true
  columns:
    titre:
      type: string(255)
      notnull: true
    # et d'autres champs
    id_auteur:
      type: integer
      notnull: true
  relations:
    Auteur:  { class: vjGuardUserProfile, onDelete: SET NULL, onUpdate: CASCADE, local: id_auteur, foreign: id }
  • generator.yml
 list:
 title: Liste des articles
 display: [titre, date_publication, Auteur]
 max_per_page: 5
 table_method: retrieveBackend
  • ArticleTable.class.php
public function retrieveBackend(Doctrine_Query $q)
{
  $rootAlias = $q->getRootAlias($q);
  $q->leftJoin($rootAlias.'.Auteur au');
  return $q;
}

Dans cet exemple, je passe de 6 requêtes Doctrine pour l’affichage de 5 lignes à 3 requêtes.

Ce n’est pas énorme mais sur 20 lignes et 3 relations, on passe de 63 requêtes à

3 requêtes !

Sisi !

Alors, abusez-en !

[Tuto] Bien utiliser le widget sfWidgetFormDoctrineJQueryAutocomplete

Enfin un tutoriel ! Cela faisait bien longtemps que je n’en avais pas publié (par faute de temps …) !

Sujet du jour : sfWidgetFormDoctrineJQueryAutocomplete

Ce widget propose un champ input de type text qui, lorsque vous tapez des données, effectue une recherche en ajax dans la base de données (ici, nous travaillons avec Doctrine) et dans le cas de résultats, qui vous les retranscrit dans une liste placée sous le champ input.

L’idée étant de rechercher rapidement et simplement des données dans une liste plus ou moins longue. Qui plus est, cela permet de charger plus rapidement la page et par la même occasion, d’éviter de créer des listes très longues.

Voici une représentation du widget en fonction :

Alors, pour le mettre en œuvre, il faut au préalable installer le plugin sfFormExtraPlugin qui l’intègre (l’installation est standard).

Pour bien comprendre la technique, je vais vous décrire chaque étape importante et vous fournir la structuration des données.

  • le schéma de données (schema.yml)
options:
  collate: utf8_general_ci
  charset: utf8
Maitre:
  connection: doctrine
  columns:
    titre:
      type: varchar(255)
      notnull: true
Eleve:
  connection: doctrine
  columns:
    titre:
      type: varchar(255)
      notnull: true
    id_maitre:
      type: integer
      notnull: true
  relations:
    Maitre: { onDelete: cascade, onUpdate: cascade, local: id_maitre, foreign: id }

J’ai ensuite généré ma base de données et deux modules d’admin generator avec Doctrine.
Je me retrouve donc avec un module de gestion des Maîtres et un autre des Elèves. Du fait du schéma, lorsqu’on rajoute un élève, on doit choisir son maître parmi la liste déroulante de maîtres. Nous placerons donc ici notre autocomplete.

Avant de l’intégrer, voici comment se présente le formulaire d’ajout d’élève :

La première chose qu’on travaille généralement dans ce cas est l’affichage d’un texte à la place des identifiants :

  • lib/model/doctrine/Maitre.class.php
class Maitre extends BaseMaitre
{
  public function __toString()
  {
    return $this->titre;
  }
}

Ce qui nous donne :

Jusque là, rien de particulier mais la liste paraît assez longue tout de même (899 entrées !) et en environnement dev, cache vidé, cette simple page met plus de 0,8s à apparaître. C’est trop.

Remplaçons cette liste déroulante par un champ autocomplete !

  • lib/form/doctrine/EleveForm.class.php
  public function configure()
  {
    $this->widgetSchema['id_maitre']->setOption('renderer_class', 'sfWidgetFormDoctrineJQueryAutocompleter');
    $this->widgetSchema['id_maitre'] = new sfWidgetFormDoctrineJQueryAutocompleter(
      array(
        'model' => "Maitre",
        'url'   => url_for("@ajax_maitre"),
        'config' => '{ max: 50}'
      )
    );
  }

On remarque qu’on remplace bien le widget d’origine (sfWidgetFormDoctrineChoice) par l’autocomplete.

On remarque également qu’on lui passe en option le modèle avec lequel travaillé et une route symfony mais qu’on peut également lui passer le nombre de lignes à afficher (ici 50).

Utilisant le helper Url, il faut penser à le charger ici :

  public function configure()
  {
    sfProjectConfiguration::getActive()->loadHelpers('Url');
    // ....
  }

Il faut donc maintenant créer cette route

  • apps/your_app/config/routing.yml
ajax_maitre:
  url: /ajax-maitre
  param: { module: maitre, action: ajaxMaitre }

Cette route est simple à comprendre : on charge la méthode executeAjaxMaitre du fichiers actions.class.php du module maitre.

Créons donc cette méthode alors !

  • apps/your_app/modules/maitre/actions/actions.class.php
  public function executeAjaxMaitre(sfWebRequest $request)
  {
    // si ce n'est pas une requête ajax, on renvoit vers du 404
    $this->forward404unless($request->isXmlHttpRequest());
    // c'est de l'ajax, on retourne donc du json
    $this->getResponse()->setContentType('application/json');
    $choices = array();
    // on récupère les données de la base de données
    $maitres = Doctrine::getTable('Maitre')->getMaitreAutocompletion($request->getParameter('q'), $request->getParameter('limit'));
    // on boucle sur les données et on charge les données dans un nouveau tableau, celui qu'on va retourner
    foreach($maitres->getData() as $m)
    {
      $choices[$m->id] = $m->titre;
    }
    // s'il y a des données, on retourne le tableau encodé en json
    if($choices != array())
    {
      return $this->renderText(json_encode($choices));
    }
  }

Il nous reste à créer la méthode getMaitreAutocompletion du modèle Maitre

  • lib/model/doctrine/MaitreTable.class.php
  public function getMaitreAutocompletion($q, $limit){
    $dq = Doctrine_Query::create()
            ->select("m.id, m.titre")
            ->from("Maitre m")
            ->where("m.titre LIKE ?","%".$q."%")
            ->limit($limit)
            ->orderBy('m.titre ASC');
    return $dq->execute();
  }

Dernière chose importante avant de tester, il faudrait peut-être intégrer jQuery non ? Commençons par télécharger la dernière version minifiée sur le site officiel, copions là dans web/js/jquery.min.js puis

  • apps/your_app/config/view.yml
  javascripts:    [jquery.min.js]

That’s all folks !

Regardons alors le résultat :

Quoi de plus simple, je vous le demande !

Ainsi s’achève ce petit tutoriel qui, je l’espère, vous sera bien utile !

A bientôt pour de nouvelles aventures sur symfony !

EDIT : Merci à Jonathan T pour l’astuce concernant le nombre de lignes affichées lors de l’autocompletion !

Intégration de la librairie XAJAX (admin generator) – Partie 2

Reprenons ou nous nous étions arrêté hier soir sur la première partie de ce tutoriel.

Il nous restait donc à créer les différentes actions :

Actions du module TOOLS
Le module tools me sert pour plusieurs choses (par exemple XAJAX + JQUERY autocompleter). Je ne vais vous proposer que le code pour XAJAX :

<?php

class toolsActions extends sfActions
{
  public function executeXajax(sfWebRequest $request)
  {
  }
}

// FONCTION XAJAX D'AFFICHAGE DE LA LISTE DES VILLES EN FONCTION DU CODE POSTAL SAISI
function xajax_alert($message){
  $objResponse = new xajaxResponse();
  $objResponse->alert($message);
  return $objResponse;
}>

Vous le voyez, c’est assez simple. Il faut bien voir aussi que nous utilisons ici une fonction très simple d’alert javascript.
La fonction executeXajax() permet ici de charger la fonction xajax_alert() qui n’est pas dans la classe toolsActions.
J’ai choisi d’utiliser une fonction simple de XAJAX car je ne prétends pas faire un tutoriel sur l’utilisation d’XAJAX mais je l’ai utilisé dans un projet afin de remplir des champs en fonction d’un autre champ ou d’afficher une liste déroulante de ville en fonction du code postal saisi dans un champ de formulaire.

Ajout dans le layout
Nous alons rajouter une ligne dans le layout qui permettra d’enregistrer la fonction que nous souhaitons appeler (le helper prend en argument un tableau donc on peut enregistrer plusieurs fonctions en même temps) :

 <?php use_helper('xajax') ?> // code ajouté hier
<?php register_functions(array("xajax_alert")) ?>
<?php init_xajax_javascript() ?>  // code ajouté hier

Nous le plaçons après avoir chargé le helper (logique :p) et avant l’initialisation du script dans le document (je ne sais pas si c’est important mais ça fonctionne ainsi :p).

Action/Template dans votre module
Voilà, il ne nous reste plus qu’à utiliser notre fonction xajax_alert() quelque part. Soit vous utilisé un module déjà créé soit vous créez un nouveau module. Je vous propose de créer un module et du coup, une nouvelle action :

public function executeAffichagexajax(sfWebRequest $request)
{
}

Bon, il faut bien se dire ici que cette action est vide et ne sert uniquement qu’à tester notre fonction xajax. Si vous voulez utiliser XAJAX dans une action différente, c’est tout aussi possible. Dans ce cas, il suffit juste d’ajouter dans le template correspondant (action = executeAffichagexajax / template = affichagexajaxSuccess.php) un appel à cette fonction :

<?php echo link_to('cliquez moi !', '', array('onclick' => 'xajax_xajax_alert("BRAVO, XAJAX FONCTIONNE !");return false;')) ?>

Voilà, si tout va bien, vous devriez voir ceci sur votre page :

Ce tutoriel est assez complexe dans le sens où il faut travailler avec beaucoup de fichiers et du coup, il se peut qu’à un endroit ou un autre, cela coince.

Quoiqu’il en soit, j’espère vous avoir aidé à comprendre comment intégrer XAJAX à symfony permettant d’offrir au framework de nouvelles fonctionnalités sympathiques :)

Si vous avez des questions, des problèmes ou autres, n’hésitez pas, j’essayerai de vous répondre rapidement.

Mon prochain article devrait concerner la conférence Symfony Live qui a débute ce jeudi. Je ne garantis pas un article pour lundi prochain mais je l’espère tout de même en début de semaine.

A bon entendeur !

Intégration de la librairie XAJAX (admin generator) – Partie 1

Suite à l’article publié la semaine dernière sur comment intégrer la librairie FPDF à symfony, je vais vous présenter aujourd’hui comment intégrer la librairie XAJAX.

Intégration de la librairie XAJAX
- téléchargez la dernière version de la librairie XJAX (page de téléchargement officielleversion 0.5 RC2 zip utilisée dans ce tutoriel)
- décompressez le fichier puis placez le répertoire xajax_core dans le répertoire lib/ de votre projet  ce qui doit donner : /path_du_projet/lib/xajax_core
- placez le répertoire xajax_js dans le répertoire /web de votre projet ce qui doit donner : /path_du_projet/web/xajax_js
- créez le fichier xajax_symfony.php dans /path_du_projet/lib/xajax_core/ :

<?php

class sf_xajax{
  static function getXajaxObj(){
    global $xajax_symfony;
    if (!isset($xajax_symfony)){
      if(sfConfig::get('sf_app')."_".sfConfig::get('sf_environment')==sfConfig::get('app_index_conf'))       $php_file="";
      else $php_file=sfConfig::get('sf_app')."_".sfConfig::get('sf_environment').".php/";
      $xajax_symfony = new xajax("/".$php_file."xajax");
    }
    return $xajax_symfony;
  }

  static function registerFunctions($functions){
    $xajax_symfony = sf_xajax::getXajaxObj();
    foreach($functions as $function){
      $xajax_symfony->register(XAJAX_FUNCTION,$function);
    }
  }

  static function processRequest(){
    $xajax_symfony = sf_xajax::getXajaxObj();
    $xajax_symfony->processRequest();
  }

  static function printJavascript(){
    $xajax_symfony = sf_xajax::getXajaxObj();
    $xajax_symfony->printJavascript("/");
  }
}
?>

- créez le fichier xajaxHelper.php dans /path_du_projet/lib/helper/ :

<?php

  function xajax_input($type,$value,$function,$event,$funcArgs,$tagParams){
    $tmpVal = "<input type=\"$type\" value=\"$value\"";
    $fArgs = join(",",$funcArgs);
    $tmpVal .= " $event=\"xajax_$function($fArgs)\"";
    foreach($tagParams as $pKey=>$pValue){
      $tmpVal .= " $pKey=\"$pValue\"";
    }
    $tmpVal .="/>";
    return $tmpVal;
  }

  function xajax_js_get_value($objId){
    return "xajax.$('$objId').value";
  }

  function include_xajax_javascript($uri){
    sf_xajax::printJavascript($uri);
  }

  function init_xajax_javascript(){
    sf_xajax::processRequest();
  }

  function register_functions($functions=array()){
    sf_xajax::registerFunctions($functions);
  }

  function xajax_start_form($id,$function,$tagParams){
    $tmpVal = "";
    foreach($tagParams as $pKey=>$pValue){
      $tmpVal .= " $pKey=\"$pValue\"";
    }
    return "<form id=\"$id\" action=\"javascript:void(null);\" onsubmit=\"xajax_$function(xajax.getFormValues('$id'));\"$tmpVal>";
  }

  function xajax_end_form(){
    return "</form>";
  }
?>

Intégration à l’admin generator
- en ligne de commande :

./symfony generate:module mon_application tools

- dans le fichier layout.php de votre application d’admin generator :
avant le doctype :

<?php use_helper('xajax') ?>
<?php init_xajax_javascript() ?>

entre les balises head :

<?php include_xajax_javascript() ?>

- dans le fichier routing.yml de votre application d’admin generator :

xajax:
  url:  /xajax
  param:  { module: tools, action : xajax }

Il nous reste à créer les actions dans le module tools et à utiliser ces actions dans d’autres modules.

Fin de la première partie. La suite demain !

Intégration de la librairie FPDF (admin generator)

Je vais vous présenter aujourd’hui comment intégrer la librairie FPDF à symfony et surtout, comment l’intégrer proprement dans une page web.

Intégration de la librairie FPDF
- téléchargez la dernière version de la librairie FPDF (page de téléchargement officielleversion 1.6 tgz utilisée dans ce tutoriel)
- décompressez le fichier dans le répertoire lib/ de votre projet (personnellement, j’ai créé un repertoire lib/tools/) ce qui doit donner : /path_du_projet/lib/fpdf16

Intégration à l’admin generator
- dans le fichier action.class.php de votre module d’admin generator :

public function executeMonPDF(sfWebRequest $request){
  // on définit le répertoire des polices (en dur ici mais il suffit de placer le paramètre dans le fichier app.yml)
  define('FPDF_FONTPATH','/path_du_projet/lib/tools/fpdf16/font/');
  // on charge la classe FPDF
  $this->pdf = new FPDF();
  // on ajoute une page au document
  $this->pdf->AddPage();
  // j'écris un texte en arial 11 gras sur fond blanc
  $this->pdf->SetFillColor(204,204,204);
  $this->pdf->SetFont("Arial",'B',11);
  $this->pdf->Write(5,'Mon premier document PDF sous symfony !');
  // nom du fichier pdf qui sera créé
  $this->file_name = "uploads/pdf/mon_pdf_sous_symfony.pdf";
  // on exporte le fichier
  $this->pdf->Output($this->file_name, "F");
  // on définit la classe du div contenant le PDF
  $this->classe = "pdf";
  // on donne un titre <h1> à la page
  $this->h1 = "Impression de mon premier PDF";
  // on appelle le template pdfSuccess.php
  $this->setTemplate('pdf');
}

- dans le fichier pdfSuccess.php de votre module d’admin generator :

<h1><?php echo $h1 ?></h1>
<div class="sf_admin_pdf">
  <object data="<?php echo $file ?>" type="text/html" codetype="application/pdf" class="object_pdf"></object>
<div>

- dans le fichier web/css/main.css du projet :

div.sf_admin_pdf{
  margin:0 auto;
  text-align:center;
  width:95%;
  height:80%;
  border:2px solid black;
}
.object_pdf{
  width:100%;
  height:100%;
}

- dans le fichier generator.yml de votre module d’admin generator :

object_actions:
  monpdf: { label: mon PDF , action: MonPDF }

Ces quatres fichiers devraient vous permettre d’intégrer un document PDF de cette manière :

Cela permet d’éviter d’avoir directement le document à télécharger (on peut le sauvegarder ou l’imprimer par exemple) ou de l’avoir en plein écran et de devoir faire Page précédente ou même de l’avoir dans une nouvelle fenêtre.

J’ai été beaucoup plus loin dans l’intégration de FPDF, j’ai créé une classe qui étend celle de FPDF qui me permet de gérer un en-tête et/ou un pied de page ou encore de créer des fonctions d’affichage différentes, etc …

Pour ce qui est de l’utilisation de FPDF, elle est assez simple et la documentation sur le site officiel (documentation + tutoriaux) est assez explicite. Si vous avez encore des soucis, d’autres tutoriaux sont disponibles par une simple recherche sur Internet.

Petite astuce, FPDF ne gère pas l’UTF-8, pensez donc à bien utiliser utf8_decode() au besoin :)

On se retrouve bientôt pour un tutoriel sur l’intégration de XAJAX !