Drupal 8 - Récupérer l'arborescence des termes (comparatif)

19/11/2018

Avec Drupal 8, il est fréquent de devoir récupérer l'arborescence des termes de taxonomy. Malheureusement, il n'existe pas de méthode du coeur qui propose cette fonctionnalité. Il n'est pas très compliqué de la mettre en oeuvre, notamment parce qu'il existe de nombreux exemples d'implémentation par la communauté.

La plupart de ces implémentations passe par l'appel à une méthode récursive. Ce n'est pas étonnant puisque la notion d'arborescence colle très bien avec la notion de récursivité. Je suis notamment tombé sur un article de Danny Sipos qui propose une méthode récursive (Loading taxonomy terms tree). Si vous ne connaissez pas son blog Web Omelette, je vous conseille d'aller y jeter un oeil, ses articles sont très pointus et toujours pertinents. 

Cependant, on n'est pas obligé de passer par une méthode récursive. En effet, grâce aux références on peut facilement créer une arborescence sans récursivité.

J'ai donc voulu savoir quelle était la meilleure méthode, quelle est la plus performante ? Je vais vous donner les deux méthodes, puis enfin les comparer. Les paris sont ouverts (pour être tout à fait honnête à ce stade de l'article je parie sur la méthode récursive qui serait la plus performante en temps mais peut-être pas forcément en ressource).

 

Méthode récursive

Comme évoqué plus haut, la méthode récursive est très bien expliquée dans cet article : Loading taxonomy terms tree. Je ne vais donc faire qu'un simple copier/coller ici pour simplifier la comparaison : 

<?php
 
namespace Drupal\mon_module\Tools
 
use Drupal\Core\Entity\EntityTypeManager;
 
/**
 * Loads taxonomy terms in a tree
 */
class TaxonomyTermTree {
 
  /**
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  protected $entityTypeManager;
 
  /**
   * TaxonomyTermTree constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
   */
  public function __construct(EntityTypeManager $entityTypeManager) {
    $this->entityTypeManager = $entityTypeManager;
  }
 
  /**
   * Loads the tree of a vocabulary.
   *
   * @param string $vocabulary
   *   Machine name
   *
   * @return array
   */
  public function load($vocabulary) {
    $terms = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vocabulary);
    $tree = [];
    foreach ($terms as $tree_object) {
      $this->buildTree($tree, $tree_object, $vocabulary);
    }
 
    return $tree;
  }
 
  /**
   * Populates a tree array given a taxonomy term tree object.
   *
   * @param $tree
   * @param $object
   * @param $vocabulary
   */
  protected function buildTree(&$tree, $object, $vocabulary) {
    if ($object->depth != 0) {
      return;
    }
    $tree[$object->tid] = $object;
    $tree[$object->tid]->children = [];
    $object_children = &$tree[$object->tid]->children;
 
    $children = $this->entityTypeManager->getStorage('taxonomy_term')->loadChildren($object->tid);
    if (!$children) {
      return;
    }
 
    $child_tree_objects = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vocabulary, $object->tid);
 
    foreach ($children as $child) {
      foreach ($child_tree_objects as $child_tree_object) {
        if ($child_tree_object->tid == $child->id()) {
         $this->buildTree($object_children, $child_tree_object, $vocabulary);
        }
      }
    }
  }
}

 

Méthode itérative

Et voici maintenant la méthode récursive que je propose. Elle n'est pas très compliquée, il s'agit essentiellement de récupérer les termes à plat, puis de les parcourir afin de les ajouter à la propriété children de leurs termes parents. On profite ici de la méthode du taxonomy storage 'loadTree' qui renvoie une arborescence... à plat.

 

<?php

namespace Drupal\mon_module\Tools

use Drupal\taxonomy\Entity\Term;

/**
 * Service de récupération d'arborescence de termes
 *
 * @package Drupal\mon_module\Tools
 */
class IteratifService {

  /**
   * Load Vocabulary Tree.
   *
   * @param string $vid
   *   Vocabulary ID to retrieve terms for.
   * @param int $parent
   *   The term ID under which to generate the tree.
   * @param int $max_depth
   *   The number of levels of the tree to return.
   *
   * @return array
   *   Return array
   */
  public function loadVocabularyTree($vid, $parent = 0, $max_depth = NULL) {

    // Get tree.
    /** @var \Drupal\taxonomy\TermStorage $taxonomy_storage */
    $taxonomy_storage = \Drupal::service('entity_type.manager')
      ->getStorage('taxonomy_term');
    $taxonomy = $taxonomy_storage->loadTree($vid, $parent, $max_depth, FALSE);

    // Get terms of the passed vid.
    $terms = $taxonomy_storage->loadByProperties(['vid' => $vid]);

    // Init result array.
    $result = [];

    foreach ($taxonomy as $taxo) {
      if ($taxo->depth == 0) {
        $result[$taxo->tid] = $terms[$taxo->tid];
      }
      else {
        foreach( $taxo->parents as $parentId ){
          if ($parentId != 0 && array_key_exists($parentId, $terms)) {
            if (!is_array($terms[$parentId]->children)) {
              $terms[$parentId]->children = [];
            }
            $terms[$parentId]->children[$taxo->tid] = $terms[$taxo->tid];
          }
        }
      }
    }
    return $result;
  }
}

 

Comparatif

Pour comparer ces deux méthodes, je me suis appuyé sur un vocabulaire comprenant 342 termes, avec 3 niveaux au maximum. Ce n'est pas énorme mais elle permet déjà de voir les différences. J'ai lancé une boucle de 1000 itérations pour chaque méthode avec un compteur de temps. J'ai lancé les deux méthodes à la suite, dans un ordre puis dans l'autre pour éviter que l'une influence l'autre. Voici la méthode : 

<?php

namespace Drupal\mon_module\Tools

class TermTreeLoadPerf {

  /**
   * Lance le test.
   */
  public function launchTest() {
    $iterationCount = 1000;

    $times = $this->testIteration($iterationCount);
    dump('--  Iteratif --');
    dump('Total: ' . array_sum($times));
    dump('Moyenne: ' . (array_sum($times) / $iterationCount));

    $times = $this->testRecursion($iterationCount);
    dump('--  Recursif --');
    dump('Total: ' . array_sum($times));
    dump('Moyenne: ' . (array_sum($times) / $iterationCount));
  }

  /**
   * Effectue un set de load en mode itératif.
   *
   * @param int $iterationCount
   *
   * @return array
   */
  protected function testIteration($iterationCount) {
    $times = [];
    $iteratifService = new IteratifService();
    for ($i = 0; $i < $iterationCount; $i++) {
      $before = microtime(TRUE);
      $iteratifService->loadVocabularyTree('criteria');
      $times[] = microtime(TRUE) - $before;
    }
    return $times;
  }

  /**
   * Effectue un set de load en mode récursif.
   *
   * @param int $iterationCount
   *
   * @return array
   */
  protected function testRecursion($iterationCount) {
    $times = [];
    $service = new TaxonomyTermTree(\Drupal::entityTypeManager());
    for ($i = 0; $i < $iterationCount; $i++) {
      $before = microtime(TRUE);
      $service->load('criteria');
      $times[] = microtime(TRUE) - $before;
    }
    return $times;
  }

}

 

Dans l'ordre itératif puis récursif, on obtient : 

Méthode Temps d'exécution pour 1000 itérations (s) Temps moyen d'une itération (s)
Méthode itératif 5.5383229255676 0.0055383229255676
Méthode récursive 24.335193157196 0.024335193157196

 

Et dans l'ordre récursif puis itératif, on obtient :  

Méthode Temps d'exécution pour 1000 itérations (s) Temps moyen d'une itération (s)
Méthode récursive 23.042543172836 0.023042543172836
Méthode itératif 5.4480402469635 0.0054480402469635


 

Conclusion

Au final, on remarque que le temps d'exécution est largement inférieur avec la méthode itérative. Effectivement la méthode récursive fait appel à la méthode loadTree de nombreuses fois, et c'est forcément assez coûteux. On remarque quand même un delta très important puisque la méthode récursive est plus de 4 fois plus coûteuse.

Ce petit comparatif nous apprend donc que parfois notre logique de développeur n'est pas forcément la plus performante et qu'il faut parfois sortir des préjugés (arborescence => récursivité) pour savoir retourner à des processus plus simple, et plus performants. Attention toutefois, c'est probablement un des cas extrêmement rares où la logique humaine est plus performante que la logique développeur.

 

Ajouter un commentaire

HTML restreint

  • Balises HTML autorisées : <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Les lignes et les paragraphes vont à la ligne automatiquement.
  • Les adresses de pages web et les adresses courriel se transforment en liens automatiquement.
Votre email ne sera pas publié mais permettra à l'administrateur de vous recontacter en cas de problème