Drupal 8 - Récupérer l'arborescence des termes (comparatif)
Comparatif de deux méthodes de chargement des termes de taxonomies en arborescence sous Drupal 8.
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 itérative 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.
Commentaires
Bonjour.
Est ce qu'il ne faudrait pas plutôt écrire "Et voici maintenant la méthode itérative que je propose" dans la partie "Méthode itérative" ?