Doctrine_Collection et hydration hiérarchisée.

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.

Partagez cet article