Navigation

Related Articles

Back to Latest Articles

Doctrine_Collection et hydration hiérarchisée.


Samuel Breton
Doctrine_Collection et hydration...

En ce moment nous sommes en train de travailler sur un site de collectionneurs et nous avons le plaisir de tomber sur quelques cas intéressants au niveau développement ce qui n’est pas pour nous déplaire. Ce site contient une grosse partie sur la gestion des zones géographiques, on a décidé d’utiliser le behavior Nestedset ce qui nous permet de gérer facilement les différentes arborescences : « Continent > Pays > état » ou « Continent > Pays > Région » ou encore « Continent > Pays / Ancien Pays ».

Je ne vais pas faire un point sur le nested, la gestion par arborescence a déjà été abordée sur ce blog. En revanche je vais vous faire part d’une découverte récente. La possibilité d’hydrater directement une collection sous sa forme d’arbre.

Pour le listing de ces zones j’ai donc choisi d’utiliser un plugin jQuery : treeTable. Comme son nom l’indique ce plugin va nous permettre une organisation d’un arbre dans une table HTML. Il faut indiquer en « id » de la balise

de notre tableau un identifiant de notre objet (ex: id= »node-1″) et à l’attribut « class » on signale de qui le noeud est l’enfant (ex: child-of-node-1 sera le fils du noeud 1).

  <div class="content-box-content">
    <table id="tree">
      <tbody>
        <?php foreach ($areas as $area) : ?>
        <?php $parent = $area->getNode()->getParent() ?>
        <tr id="node-<?php echo $area->getId() ?>" <?php echo $parent ? 'class="child-of-node-'.$parent->getId().'"' : '' ?>>
          <td><?php echo $area->getName() ?></td>
          <td>
            <?php echo link_to(__('module_area_list_action_add_children'), 'area_new', $area->getType(), array('query_string' => 'parent_id='.$area->getId())) ?>
          </td>
          <td>
            <?php echo link_to(__('text_action_delete'), '@area_delete?id='.$area->getId(), array('method' => 'delete', 'confirm' => __('text_action_delete_confirm'))) ?>
          </td>
        </tr>
        <?php endforeach ?>
      </tbody>
    </table>
  </div>

Voici le résultat :

treeTableArea

Le rendu correspond à ce que souhaite mon client, lorsque l’on clique sur une branche, par exemple Afrique ou Europe les lignes suivantes se déplient et laissent apparaitre les enfants. Tout va pour le mieux dans le meilleur des mondes, simplement quand on regarde de plus près on s’aperçoit que pour un listing de simplement 12 zones je fais 29 requêtes, que va t’il se passer lorsque je vais avoir toutes mes zones renseignées, la réponse est : +700 requêtes. Le souci vient de la méthode getNode()->getParent() qui exécute une nouvelle requête à chaque appel. En effet pour lier une ligne à son parent j’ai besoin de connaître l’ID du père.

Pour remédier à cela je vais donc utiliser l’Hydratation hiérarchisée, que l’on peut créer à l’aide d’une Doctrine_Query qu’on exécutera comme suit :

Doctrine_Core::getTable('Area')->
   createQuery('a')
  execute(array(), Doctrine_Core::HYDRATE_RECORD_HIERARCHY);

Sinon on peut directement utiliser la méthode toHierarchy() sur notre collection, qui nous hydratera directement cette dernière. Chaque élément contenant alors des enfants aura une clé __children, qui n’est autre qu’un tableau des éléments enfants.

Je vais donc remplacer mon code et utiliser un helper pour le rendu d’une ligne ce qui permettra de faire de la récursivité plus facilement dans mon code.

Je crée donc l’helper suivant :

// apps/backend/lib/helper/AreaHierarchyHelper.php
 
<?php
 
/**
 * Permet l'affichage en arbre dans le listing des zones
 *
 * @param Area $area
 * @param int $parent
 * @return string
 */
function render_row($area, $parent = null)
{
 
  // Formatage du HTML
  $html = sprintf('
<tr id="node-%s"%s>
  <td>%s</td>
  <td>%s</td>
  <td>%s</td>
  <td>%s</td>
  <td>%s</td>
</tr>',
      $area->getid(), (null === $parent ? '' : sprintf(' class="child-of-node-%s"', $parent)),
      $area->getName(),
      'ID : '.$area->getId(),
      link_to(__('module_area_list_action_add_children'), 'area_new', $area->getType(), array('query_string' => 'parent_id='.$area->getId())),
      link_to(__('text_action_edit'), 'area_edit', $area, array('class' => 'area-edit')),
      link_to(__('text_action_delete'), '@area_delete?id='.$area->getId(), array('method' => 'delete', 'confirm' => __('text_action_delete_confirm'))));
 
  // Récursivité pour créer les lignes des fils
  foreach ($area->get('__children') as $child)
  {
    $html .= render_row($child, $area->getid());
  }
 
  return $html;
}

Désormais sur ma page, il me suffit de charger mon helper et d’écrire le code suivant :

<?php use_helper('AreaHierarchy') ?>
 
<div class="content-box">
  <div class="content-box-header">
  </div>
  <div class="content-box-content">
    <table id="tree">
      <tbody>
        <?php foreach ($areas->toHierarchy() as $area) : ?>
          <?php echo render_row($area) ?>
        <?php endforeach ?>
      </tbody>
    </table>
  </div>
</div>

Le résultat est exactement le même au niveau de l’affichage en revanche je passe désormais à 5 requêtes et sur les données définitives, j’obtiens toujours 5 requêtes et ce quelque soit le nombre de résultat dans ma collection. J’ai donc bénéficié des avantages de Doctrine et surtout des Doctrine_Hydrator pour diminuer le nombre de requêtes.

Show Comments (10)

Comments

  • Christophe Willemsen

    Super.. Venant d’apprendre Symfony, c’est une des fonctions que je cherchais absolument (NestedSet + hydration) pour l’implémentation d’un catalogue en ligne.

    Merci…..

    • Article Author
  • Thibault

    Rhaaa ! J’ai passé la journée d’hier à recoder un truc qui fait la même chose en moins bien. Si j’avais pu trouver ça plus tôt…

    • Article Author
  • Florian

    Super interessant cette methode d’hydration, je connaissais pas.

    Par contre, pourquoi passer par un Helper alors qu’un partial aurait surement été mieux ?

    • Article Author
  • yoye

    C’est simplement pour éviter de recharger le context. Dans l’exemple du foreach sur une liste de pays je devrais recharger mon context et instancier la classe sfPartialView + de 300 fois.

    • Article Author
  • Jf

    Je ne comprens pas quel est le type de données pour areas()

    dans actions.class, je renvoie un $this->areas = Doctrine_Core::getTable(‘Category’)->createQuery()->execute();

    Mais j’ai un message d’erreur qui me dit que areas doit être un string

    Fatal error: Function name must be a string

    Merci

    • Article Author
  • Jf

    Je ne comprens pas quel est le type de données pour areas()

    dans actions.class, je renvoie un $this->areas = Doctrine_Core::getTable(\’Category\’)->createQuery()->execute();

    Mais j\’ai un message d\’erreur qui me dit que areas doit être un string

    Fatal error: Function name must be a string

    Merci

    • Article Author
  • Jf

    Ok, il y a une faute dans le tuto, c’est areas sans les ()

    merci beaucoup

    • Article Author
  • yoye

    Effectivement, je viens de corriger la coquille.

    • Article Author
  • r4cker

    Super tuto qui me permet d’avancer un petit peu, mais quelqu’un aurait-il un exemple avec symfony2?

    • Article Author
  • r4cker

    Super tuto qui me permet d\’avancer un petit peu, mais quelqu\’un aurait-il un exemple avec symfony2?

    • Article Author

Recevez nos articles