Aller au contenu principal

ListBuilder, pagination et ordre de tri

Image
Méthode load() d'un ConfigEntityListBuilder
Par Thierry V le
Dans la catégorie

Le problème

Je me suis retrouvé confronté à un problème assez particulier ce soir.

J'ai une liste d'entités qui affichent des colonnes bootstrap. Ça fait partie de mon module Responsive Builder, celui qui me permet de générer toutes les largeurs de colonnes sur un site, puis de générer des styles adaptatifs, les styles d'image qui vont avec, et même les view-modes finaux à utiliser avec des entités.

Dans l'optique d'une refonte de ce module pour pouvoir le mapper d'une manière plus simple à n'importe quel type d'entité, je me suis rendu compte que l'affichage des données était un peu foireux. Je voulais trier les lignes du ListBuilder avec les infos suivantes : 

  • thème
  • contexte
  • breakpoint (xs, sm, md, lg, xl, xxl)
  • colonne (1, 2, 3, 4, 5, 6, etc... les fameuses 12 colonnes des thèmes bootstrap)

Je me suis dit : facile, je surdéfinis la méthode getEntityListQuery() de mon ListBuilder, j'ajoute les ->sort(...) qu'il faut et hop, le tour est joué.

Bah non... EntityListBuilder, la classe 2 niveaux au dessus de ma classe, fait ça : 

  protected function getEntityListQuery(): QueryInterface {
    $query = $this->getStorage()->getQuery()
      ->accessCheck(TRUE)
      ->sort($this->entityType->getKey(static::SORT_KEY));

    // Only add the pager if a limit is specified.
    if ($this->limit) {
      $query->pager($this->limit);
    }
    return $query;
  }

Cette coquine applique déjà un ordre de tri pour la query en question. Au départ, j'ai testé ça dans ma classe : 

protected function getEntityListQuery(): QueryInterface {
  $query = parent::getEntityListQuery();
  $query
    ->sort('theme', 'ASC')
    ->sort('context', 'ASC')
    ->sort('column', 'ASC')
    ->sort('delta', 'ASC');
  return $query;
}

Mais c'était avant de voir ce que faisait EntityListBuilder... Donc je me suis dit que j'allais éviter d'appeler la classe parent et reconstruire entièrement la variable $query. Mais là encore, ça finissait par ne pas fonctionner. Il semblerait que Drupal aille chercher les données ailleurs que dans la base de données, possiblement dans un tableau de données déjà chargé en mémoire, ou quelque chose du genre... Donc bien que je lui donne tous les tris nécessaires, il me retournait tout de même un tableau qui n'était pas ordonné correctement.

La solution

Après différents tests, je me suis rendu à l'évidence qu'il fallait que je fasse un petit hack du système pour que ça soit fonctionnel. Ce sont des entités de config, il y en aura tout au plus quelques centaines dans le système. J'ai donc pris la décision suivante : 

Charger TOUTES les entités, les trier correctement, récupérer les identifiants triés puis passer ledits identifiants à la queryInterface pour qu'elle me retourne un tableau déjà trié et paginé. Mais surtout, appliquer la pagination manuellement. Car mon problème était là. Si je passais un set de 50 identifiants directement à la QueryInterface, elle allait considérer qu'il n'y avait que 50 éléments dans le tableau, et comme la limite est de 50, alors Drupal n'affichait pas du tout de pagination.

Au final, voici le code utilisé : 

  /**
   * {@inheritDoc}
   */
  protected function getEntityListQuery(): QueryInterface {
    // Load and sort ALL entities.
    $entities = $this->getStorage()->loadMultiple();
    uasort($entities, function ($a, $b) {
      $cmp = strcmp($a->get('theme'), $b->get('theme'));
      if ($cmp !== 0) {
        return $cmp;
      }

      $cmp = strcmp($a->get('context'), $b->get('context'));
      if ($cmp !== 0) {
        return $cmp;
      }
      $cmp = $a->get('delta') <=> $b->get('delta');
      if ($cmp !== 0) {
        return $cmp;
      }

      $cmp = $a->get('column') <=> $b->get('column');
      if ($cmp !== 0) {
        return $cmp;
      }

      return $a->id() <=> $b->id();
    });

    $total = count($entities);

    // Initialize pager manually with real total.
    /** @var \Drupal\Core\Pager\PagerManagerInterface $pager_manager */
    $pager_manager = \Drupal::service('pager.manager');
    $pager = $pager_manager->createPager($total, $this->limit);
    $page = $pager->getCurrentPage();

    // Extract correct fragment.
    $offset = $page * $this->limit;
    $page_ids = array_keys(array_slice($entities, $offset, $this->limit));

    // Return basic query without pager.
    return $this->getStorage()->getQuery()
      ->accessCheck(TRUE)
      ->condition('id', $page_ids, 'IN');
  }

  /**
   * {@inheritDoc}
   */
  public function load() {
    $entity_ids = $this->getEntityIds();
    $entities = $this->storage->loadMultiple($entity_ids);
    return array_replace(array_flip($entity_ids), $entities);
  }

Ce que je fais : 

  1. chargement de TOUTES les entités, puis tri correct sur ces dernières, selon mes critères de tri.
  2. Récupération du nombre total d'entités dans mon tableau, afin de pouvoir l'indiquer au système de pagination.
  3. Initialisation manuelle du système de pagination en lui indiquant le total et la limite d'affichage.
  4. Récupération de la page en cours
  5. Extraction d'un sous-tableau correspondant exactement aux identifiants dans la plage de données demandée.
  6. Construction d'une QueryInterface à laquelle je passe simplement les identifiants.
  7. Dans la méthode load() que j'ai également surdéfinie, je charge toutes les entités et je les remets dans l'ordre par rapport à mon tableau d'entités (qui lui est déjà ordonné selon mon choix, via la méthode précédente).

Et là, magie, ça fonctionne !! 

Conclusion

Drupal est un magnifique outil, il permet de faire des choses vraiment intéressantes et surtout de les faire proprement. Mais dans certains cas, la logique peut s'avérer un peu obscure, et on passe vite beaucoup de temps à trouver une solution à un petit problème qui semble trivial en premier lieu.

Dans ce cas particulier, il m'a fallu un certain moment avant de comprendre pourquoi l'ordre de tri n'était pas respecté, pourquoi les sets de données retournés semblaient ordonnés d'une manière incompréhensible... Je voyais bien que rajouter mes tris influençaient le résultat, mais pas comme je le demandais. Et j'ai du batailler un peu pour trouver une solution convenable, sans pour autant sacrifier trop de temps et de performances. Ici, charger toutes les données à la place de n'en charger qu'un set n'a pas un gros impact, vu que les données sont des entités de configuration et qu'elles ne sont pas destinées à être présentes par millier ou plus. Le panneau d'affichage est dans l'interface d'administration, et ce n'est pas le genre de panneau qu'on visite à de multiples reprises. J'ai donc fait le choix de me "contenter" de cette solution.