[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 !

Conférence symfony : « The State of Symfony 2″

Petit billet pour vous informer de la tenue prochaine d’une nouvelle conférence Symfony. Petite nouveauté car celle-ci se tiendra online.

Il y aura 2 sessions :

  • le 22 juin à 10h00
  • le 23 juin à 17h00

Il faut compter environ 3h de conférence (30 minutes par intervention).

Au programme :

  • Tests fonctionnels et unitaires
  • Propel 1.5 pour Symfony 2.0
  • Le nouveau framework de formulaire
  • Améliorations diverses
  • Intégration de Doctrine 2 à Symfony 2
  • Nouveauté « The Symfony 2 killer feature »

Que du bon en fait !

L’inscription, c’est par ici et le site ici.

Symfony Live 2010 & Symfony 2

Pour commencer, toutes mes excuses aux personnes (s’il y en avait :x ) qui voulaient me suivre hier sur mon blog lors de la seconde journée du Symfony Live 2010. En effet, un léger problème a rendu mon édition d’article impossible.

Bref, de toute façon, si vous me suivez, vous devez également suivre d’autres blogs qui vous ont sans doute fait partager cet événement.

SfLive2010

Je ne vais pas revenir sur toute cette conférence qui était ma foi fort intéressante mais très fatiguante (dieu sait que le français n’est pas le spécialiste des langues étrangères …).
Je vous laisse découvrir la plupart des présentations sur le blog de S.I.G.B.

Ce qu’il faut retenir d’important de ces deux jours : Symfony 2 & Doctrine 2 ! Pourquoi pas le reste, tout simplement parce qu’il s’agissait soit de présentation, soit d’expériences. C’était fort intéressant mais je pense que ce qu’attendaient les gens était vraiment ces deux choses.

Doctrine 2

Doctrine a été 100% réécrit pour PHP 5.3, il est environ 3 fois plus rapide que Doctrine 1, la notion de magie est appelée à disparaître (ou presque) donc les méthodes findByNom, c’est fini !
Tout va être entité dans Doctrine 2. Pour plus d’infos, je vous laisse regarder la présentation de Jonathan.

Symfony 2

Symfony 2.0 is awesome !

dixit Fabien.
Je ne peux pas tout raconter sur ce qui s’est dit hier, il y a trop de choses mais vous verez que Symfony 2 (et non symfony 2 !) est plus configurable, plus flexible, tout en mieux en fait :o
Il est environ 3 fois plus rapide que sf 1.4 et zf 1.1, 6 que cakePHP et surtout, il n’utilise que la moitié de la mémoire nécessaire à sf 1.4 et zf !
Bref, une présentation est plus parlante : Symfony 2.0 by Fabien Potentier.

[DAY 1] Symfony live 2010 #sflive2010

[MAJ4] Hum ! Intéressant … Doctrine 2 ! Que vous dire à part qu’il a été réécrit à 100% par rapport à Doctrine 1 et ce pour PHP 5.3, qu’il est beaucoup plus rapide (hydratation de 5000 objets : D1 = 4.3s – D2 = 1.4s) et qu’il devrait voir le jour en version stabled’ici 6-12 mois. Plus d’informations sur le site officiel de Doctrine.

[MAJ3] Voici le conseil de la journée : DO NOT USE SFCONTEXT IN YOUR FILES ! Par exemple, dans vos formulaires, évitez le sfContext::getInstance()->getUser(), préférez passer le context ($this->getContext()) ou le user directement ($this->getContext()->getUser()) à votre form (new myForm(null, array(‘context’ => $this->getContext()))).
Nous avons eu un excellent exposé sur les migrations avec Doctrine et je vous conseille d’y jeter un coup d’oeil !

[MAJ2] Enfin une vraie connexion internet !!!! It’s lunch time ! Et avant cela, John Cleveley nous a donné un certain nombre d’astuces sur l’admin generator (du dynamique max_per_page, des plugins intéressants, l’importance de recourir à des thèmes personnalisés, …) et juste avant le repas, deux personnes de chez Microsoft nous ont présenté le cloud computing avec Windows Azure et leurs nombreux efforts envers l’open-source (PHP, MySQL, …).

[MAJ1] Après une brève présentation de fabien potentier, le premier intervenant (thomas rabaix) nous a expliqué les principes de l’internationalisation et de la localisation avec symfony puis il nous a présenté un plugin de traduction dynamique (mgI18nPlugin).

C’est parti pour ce sf live 2010!
Je vais essayer de vous le faire partager dès que possible. Le plus dur étant de tenir la traduction (et oui je ne suis pas bilingue unfortunetly).
Un grand remerciement à l’équipe de SensioLabs qui m’a invitée à cet événement !

Symfony Live 2010 coming !

Pas beaucoup de nouvelles ces derniers temps (cours du soir, examens, taf, blog en développement, …) mais ça va revenir !

Mais bon, ce n’est pas le propos du jour ! Non, aujourd’hui, je tenais juste à vous rappeler la tenue la semaine prochaine du Symfony Live 2010.
Cet événement, déjà très intéressant l’an dernier, risque de l’être encore plus cette année ! Ca va parler internationalisation, admin generator, Zend Framework ou encore Doctrine 2.0 mais surtout Symfony 2.0 ! Eh oui ! La première release de sf2 va être dévoilée mercredi 17 février !

Qui plus est, c’est l’occasion de rencontrer la communauté mais surtout la core team et de mesurer l’étendue de symfony à travers le monde.

Je vous conseille de ne pas manquer cet événement ! Il est encore possible de s’y inscrire donc, rendez-vous là-bas !

Conférence Symfony Live 2010

Une petite news juste pour rappeller (ou pour apprendre qui sait :p) que vous pouvez vous inscrire à la conférence Symfony Live 2010 qui se tiendra les 16 et 17 février 2010 à Paris (plus d’infos).
Sachez que cette conférence ne se tiendra pas en français mais en anglais !

De plus, il est prévu une journée d’entrainement sur symfony le 15 février (pas plus d’infos pour l’instant sur le contenu de l’entrainement).

Le programme de cette conférence est très intéressant :
- working with the admin generator
- playing with the symfony components
- Doctrine 2
- Symfony 2 revealed (présentation de l’utilisation de sf 2 et publication de la version alpha !)
- et bien d’autres

Cette conférence (bien qu’en anglais) semble vraiment dans le prolongement de la précédente et je suis sûr que l’ambiance sera aussi bonne que la première !

Qu’attendez-vous ? Allez vous inscrire !!!

Pourquoi optimiser les requêtes Doctrine ?

Bonjour à tous !

Et oui, me revoilà après plusieurs mois sans post. Il faut dire qu’entre le boulot, les cours du soir et le sport, le temps est assez limité pour le reste ! Mais bon, je profite d’avoir un petit moment pour vous faire partager une expérience qui, pour beaucoup, semble évidente mais à laquelle on ne pense pas suffisament : Optimiser une requête Doctrine

Dans l’admin generator, on pense souvent à modifier la requête de sélection de Doctrine (pour l’affichage de la liste) en intégrant des leftJoin ou innerJoin (ceux qui ne le font pas encore, pensez-y !).

Je le fais toujours mais dans mes modules de frontend (en général, de l’affichage de données en base), je ne le faisais pas (sans doute parce que mes requête étaient simples).
Sur un projet, j’ai pu découvrir le bénéfice non négligeable d’optimiser la requête d’affichage des données, que ce soit au niveau du nombre de requêtes mais également au niveau du temps d’affichage de la page (plus précisément, au niveau du temps de calcul de l’actin d’index).

Pour que ce soit plus parlant, regardons quelques printscreen :
- utilisation de la requête générée par Doctrine

Requête non optimisée

- utilisation d’une requête (et d’une action) optimisée

Voilà, je pense que c’est suffisamment parlant non ?
Pour information, j’ai rafraichi plusieurs fois la page afin d’obtenir des valeurs lorsque le cache est utilisé.

En non optimisé, Doctrine effectue 129 requêtes (3.98ms) et une fois optimisée, UNE seule requête est effectuée (0.01ms).
Le temps étant en ms, c’est assez négligeable certes mais pour une base de données énorme avec beaucoup d’accès, le ratio est de 400 ! De plus, le fait d’avoir un certain nombre de requêtes (hydratation de base Objet) rentre en compte dans le temps de calcul de l’action dont elle dépend et ici, le ratio est encore important 6 !

Afin de vous ouvrir un peu plus les yeux, il faut voir le schema de la base afin de comprendre la « complexité » des relations :

#schema.yml
Etat:
  tableName: etat
  columns:
    titre:
      type: string(255)
      notnull: true
    color:
      type: string(255)
      notnull: true
Comment:
  actAs:
    Timestampable: ~
  tableName: comment
  columns:
    titre:
      type: string(255)
    comment:
      type: clob
      notnull: true
    user_id:
      type: integer(4)
  relations:
    sfGuardUser:
      type: one
      foreignType: many
      local: user_id
      foreign: id
      foreignAlias: Users
Tache:
  actAs:
    Timestampable: ~
  tableName: tache
  columns:
    activite:
      type: string(255)
      notnull: true
    id_etat:
      type: integer
      notnull: true
  relations:
    Etat:  { onDelete: CASCADE, local: id_etat, foreign: id }
    TacheComments:
      class: Comment
      refClass: TacheComment
      local: id_tache
      foreign: id_comment
    sfGuardUsers:
      class: sfGuardUser
      refClass: TacheUser
      local: id_tache
      foreign: id_user
TacheUser:
  tableName: tache_user
  columns:
    id_tache:
      type: integer
      notnull: true
    id_user:
      type: integer(4)
      notnull: true
  relations:
    Tache:  { onDelete: CASCADE, local: id_tache, foreign: id }
    sfGuardUser: { onDelete: CASCADE, local: id_user, foreign: id }
TacheComment:
  tableName: tache_comment
  columns:
    id_tache:
      type: integer
      notnull: true
    id_comment:
      type: integer
      notnull: true
  relations:
    Tache:  { onDelete: CASCADE, local: id_tache, foreign: id }
    Comment: { onDelete: CASCADE, local: id_comment, foreign: id }

Voici également la requête optimisée (je ne sélectionne que les données nécessaires et j’hydrate en array afin de limite les ressources nécessaires et sachant qu’il s’agit d’affichage de données, c’est parfait !) :

// TacheTable.class.php
  public function retrieveTacheWithJoins(){
    return Doctrine_Query::create()
          ->select('
              t.*,
              e.*,
              tc.id_comment,
              tu.id_user,
              c.titre, c.comment, c.user_id, c.updated_at,
              cu.username,
              u.username
            ')
          ->from('Tache t')
          ->leftJoin('t.Etat e')
          ->leftJoin('t.TacheComment tc')
          ->leftJoin('tc.Comment c')
          ->leftJoin('c.sfGuardUser cu')
          ->leftJoin('t.TacheUser tu')
          ->leftJoin('tu.sfGuardUser u')
          ->execute(array(), Doctrine::HYDRATE_ARRAY);
  }

Et pour finir, je vous propose les deux actions (en premier, l’action optimisée avec l’hydratation array et en second, l’ancienne action avec l’hydratation objet) :

//actions.class.php
  public function executeIndex(sfWebRequest $request)
  {
    $type = $request->getParameter('type');
    $this->etat = $request->getParameter('etat');
    $this->taches = Doctrine::getTable('Tache')->retrieveTacheWithJoins();
    $this->tab = array();
    $this->comments = array();
    foreach($this->taches as $tache){
      $this->users = array();
      foreach($tache['TacheUser'] as $user){
        $this->users[] = $user['sfGuardUser']['username'];
      }
      $this->users = implode(" ", $this->users);
      $this->tab[$tache['id']] = array(
                                        'activite' => $tache['activite'],
                                        'etat' => $tache['Etat']['titre'],
                                        'etat_color' => $tache['Etat']['color'],
                                        'users' => $this->users,
                                    );

      foreach($tache['TacheComment'] as $comment){
        $this->comments[$tache['id']][] = array(
                                              'user' => $comment['Comment']['sfGuardUser']['username'],
                                              'titre' => $comment['Comment']['titre'],
                                              'date' => date('d/m/Y', strtotime($comment['Comment']['updated_at'])),
                                              'comment' => $comment['Comment']['comment'],
                                            );
      }
    }
    $this->pdf = new pdf('L');
    $this->setTemplate($type);
  }

  public function executeOldIndex(sfWebRequest $request)
  {
    $type = $request->getParameter('type');
    $this->etat = $request->getParameter('etat');
    $this->taches = Doctrine::getTable('Tache')->findAll();
    $this->tab = array();
    $this->comments = array();
    foreach($this->taches as $tache){
      $this->users = array();
      foreach($tache->TacheUser as $user){
        $this->users[] = $user->getSfGuardUser();
      }
      $this->users = implode(" ", $this->users);
      $this->tab[$tache->getId()] = array(
                                        'activite' => $tache->getActivite(),
                                        'etat' => $tache->getEtat(),
                                        'etat_color' => $tache->getEtat()->getColor(),
                                        'users' => $this->users,
                                    );

      foreach($tache->TacheComment as $comment){
        $this->comments[$tache->getId()][] = array(
                                              'user' => $comment->getComment()->getSfGuardUser(),
                                              'titre' => $comment->getComment()->getTitre(),
                                              'date' => date('d/m/Y', strtotime($comment->getComment()->getUpdatedAt())),
                                              'comment' => $comment->getComment()->getComment(),
                                            );
      }
    }
    $this->pdf = new pdf('L');
    $this->setTemplate($type);
  }

Voilà, je pense que c’est assez explicite, j’obtiens exactement le même rendu avec l’une ou l’autre des actions et des requêtes et j’avoue avoir été vraiment impressionné sur ce coup là !
Morale : toujours chercher à optimiser ses requêtes !

symfony 1.2.9 & 1.3.0 alpha1

Retour de vacances et quelle surprise de voir deux nouvelles version de symfony disponibles !

symfony 1.2.9
47 mises à jour ou correctifs au programme de cette version mineure, avant-dernière version de la branche 1.2. Qui dit version mineure dit peu de modifications : Doctrine passe de la version 1.0.10 à la 1.0.12, quelques correctifs pour les formulaires et l’admin generator, la désactivation par défaut du cache du routing, une mise à jour du framework de test Lime et surtout, la compatibilité quasi assurée avec PHP 5.3 !

Je vous laisse découvrir tout cela directement sur le site officiel.

symfony 1.3.0 alpha1
En voilà une bonne nouvelle ! La première version testable de la prochaine branche de symfony ! Bon ok, il s’agit d’une version alpha non stable mais c’est déjà un premier aperçu du futur proche de symfony.
Je ne vais pas citer toutes les nouveautés ou modifications (vous pouvez les retrouver ici) mais juste les plus importantes :
- hausse importante des performances (réécriture importante du framework de routing, nouvelle tâche permettant de mettre en cache la structure complète des répertoires du projet, …)
- Doctrine est maintenant l’ORM par défaut de symfony (Propel est toujours utilisable)
- ajout du mailer Swift Mailer par défaut dans le core de symfony (un plugin de moins à installer !)
- de nombreuses amélioration du framework de formulaires
- ajout de deux entrées dans la web debug toolbar : « view » et « mailer »
- nouvel autoloader pour l’environnement de développement évitant le récurrent symfony cc !

Voilà quelques unes des nombreuses nouveautés de la version 1.3.0 ! N’hésitez pas à la tester et à envoyer vos feedback à l’équipe de symfony !

[Doctrine vs Propel] symfony 1.3 et 2.0

Une petite information que vous connaissez sans doute déjà si vous avez lu ce post sur le blog officiel : Doctrine devient l’ORM par défaut sur la version 1.3 de symfony.

Cela veut-il dire que Propel est mort chez symfony ? Non absolument pas … enfin pas tout de suite :p

Fabien Potentier nous a assuré que Propel serait toujours supporté et de la même manière pour toute la ligne 1.x mais qu’à partir de la version 1.3 (qui sortira semble-t-il en novembre), Doctrine deviendra l’ORM par défaut.

Par contre, Doctrine deviendrait pour la version 2.0 le seul et unique ORM supporté par symfony. En effet, Propel semble être un peu au point mort et sachant que Jonathan Wage fait partie de la Core Team de symfony, cela semblait logique.

Quoiqu’il en soit, la version 2.0 est encore embryonaire et ne sera pas là l’an prochain (enfin, c’est ce que j’ai ressenti des dires de Fabien).

Voilà, juste pour clarifier les choses sur ce point là :)

Bonne journée à tous !