Filtrer des contenus par année avec Views sur Drupal 8

Un calendrier sur un smartphone

Il n'est pas rare de devoir proposer de filtrer certains contenus en fonction de dates, et notamment en fonction de l'année. Comment filtrer des contenus depuis une vue selon les années basées sur un champ date ? Nous disposons d'une solution immédiate en utilisant les modules Search API couplé à Facets. Ce dernier module peut nous permettre très facilement d'ajouter une facette, à une vue, basée sur un champ date de nos contenus, et de choisir la granularité (année, mois, jour) qu'on souhaite exposer aux visiteurs. Mais si on ne dispose pas de ces deux modules pour d'autres raisons, cela peut être dommage de les installer juste pour cela. On peut arriver à nos fins assez rapidement avec une option native à Views, les arguments contextuels. Découvrons en quelques images comment y arriver.

Création de la vue des contenus à filtrer par année

Imaginons que nous disposons d'un type de contenu Bulletin, qui dispose d'un champ Date, et que nous souhaitons filtrer par année. Pour ce faire, nous avons conçu une vue qui liste tous les contenus de type bulletins. Voici la configuration générale, très classique, de la vue.

View bulletin general configuration

On distingue dans les paramètres de notre vue une section Contextual filters, ou les arguments contextuels en français. Ces arguments contextuels peuvent être ajoutés et configurés à une vue afin de pouvoir filtrer ses résultats depuis ces arguments. Ces arguments peuvent être configurés pour être fournis depuis l'url de la vue, ou depuis un contexte spécifique, depuis un champ particulier d'un contenu courant (si nous affichons la vue en parallèle d'un contenu), depuis un paramètre de la requête, etc. En fait les possibilités sont infinies et nous pouvons aussi fournir notre propre logique aux arguments contextuels de façon très aisée en implémentant un type de Plugin de Views, les @ViewsArgumentDefault. Enfin ces arguments contextuels sont créés par rapport à un champ particulier des contenus que vous visualisons avec Views.

Ajout et configuration de l'argument contextuel

Nous allons ajouter un argument contextuel à notre vue, en utilisant des types de champs un peu spécifique qui nous permettent d'obtenir des valeurs agrégées de champs Date. Nous cliquons sur le bouton pour ajouter un argument contextuel.

Add contextual filter

 

Et nous allons rechercher notre champ Date (field_date) de notre type de contenu Bulletin sous une forme agrégée par année (sous la forme YYYY).

Après avoir validé notre choix, nous obtenons le panneau de configuration de notre argument contextuel.

settings contextual filter

Vu que nous souhaitons filtrer nos contenus depuis un paramètre de la requête, nous allons configurer le panneau Quand l'argument contextuel n'est pas dans l'url. Notons que si nous avions voulu filtrer ces contenus depuis l'URL directement (sous la forme par exemple /bulletins/2017) la configuration est quasi inexistante.

Nous allons choisir l'option Fournir une valeur par défaut, et sélectionner comme type de valeur par défaut un Paramètre de requête. Nous donnons un nom à notre paramètre (year), lui fournissons une valeur de repli si le paramètre de requête n'est pas présent (all). Et nous configurons une valeur d'exception (all), valeur qui, si elle est reçue par notre paramètre de requête, nous permettra d'ignorer notre argument, et donc d'afficher tous les résultats.

Il ne nous reste plus qu'à enregistrer, puis tester notre vue.

view filtered by year

Et il nous suffit d'ajouter à la main dans l'url le paramètre ?year=2017 pour filtrer les contenus de l'année 2017.

Ajout d'un filtre exposé personnalisé

Il ne nous reste plus qu'à ajouter une liste de sélection dans les filtres exposés de la vue pour offrir au visiteur une interface pour sélectionner l'année voulue.

Vous avez noté, dans la configuration générale de la vue, que le champ Tags a été ajouté comme critère de filtre et a été également exposé. Ceci n'est pas innocent, car cela nous permet d'obtenir le formulaire de sélection déjà opérationnel, formulaire dans lequel nous n'aurons plus qu'à rajouter notre option pour les années.

Pour ce faire, nous créons un petit module, que nous intitulons my_module, et nous allons altérer ce formulaire.

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_views_exposed_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  if (isset($form['#id']) && $form['#id'] == 'views-exposed-form-bulletins-page') {
    $options = [
      'all' => t('- All -'),
      '2015' => '2015',
      '2016' => '2016',
      '2017' => '2017',
    ];

    $form['year'] = [
      '#title' => new TranslatableMarkup('By year'),
      '#type' => 'select',
      '#options' => $options,
      '#size' => NULL,
      '#default_value' => 'all',
    ];
  }
}

Nous rajoutons au formulaire exposé correspondant à la vue dont l'identifiant est bulletins, et à l'affichage dont l'identifiant est page, un simple élément de type select dont le nom year correspond au paramètre de requête défini dans les arguments contextuels de la vue.  Et nous lui fournissons quelques options, hardcodées ici, sur les années disponibles et bien entendu avec notre valeur d'exception all pour pouvoir retourner tous les résultats sans filtre aucun.

Filter year added

Et voilà. Nous disposons d'une simple vue, avec un minimum de code, qui nous permet de filtrer des contenus par date selon une granularité annuelle, granularité que nous pouvons modifier à volonté en modifiant l'argument contextuel de la vue.

Rendre dynamique les options du filtre

Les options que nous avons fournies ne risquent pas d'être pérennes avec le temps. Que fait-on en 2018 ? On modifie le code ? Améliorons un peu notre vue pour rendre dynamiques les années proposées.

Nous allons simplement depuis l'altération du formulaire exposé effectuer une requête sur tous les contenus bulletins pour récupérer toutes les dates et proposer les différentes années disponibles. Mais parce que ce type de requête peut être coûteuse, pour un simple champ liste de sélection, nous allons utiliser la Cache API de Drupal pour mettre en cache ces résultats et ne pas devoir les recalculer à chaque chargement de page.

Améliorons le snippet vu ci-dessus.

/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_views_exposed_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  if (isset($form['#id']) && $form['#id'] == 'views-exposed-form-bulletins-page') {

    $options = &drupal_static(__FUNCTION__);
    if (is_null($options)) {
      $cid = 'my_module:bulletin:year';
      $data = \Drupal::cache()->get($cid);
      if (!$data) {
        $options = [];
        $options['all'] = new TranslatableMarkup('- All -');
        $query = \Drupal::entityQuery('node');
        $query->condition('type', 'bulletin')
          ->condition('status', 1)
          ->sort('field_date', 'ASC');
        $result = $query->execute();
        if ($result) {
          $nodes = Node::loadMultiple($result);
          foreach ($nodes as $node) {
            $date = $node->field_date->value;
            if ($date) {
              $date = new DrupalDateTime($date, new DateTimeZone('UTC'));
              $year = $date->format('Y');
              if (!isset($options[$year])) {
                $options[$year] = $year;
              }
            }
          }
        }

        $cache_tags = ['node:bulletin:year'];
        \Drupal::cache()->set($cid, $options, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
      }
      else {
        $options = $data->data;
      }

    }
    
    $form['year'] = [
      '#title' => new TranslatableMarkup('By year'),
      '#type' => 'select',
      '#options' => $options,
      '#size' => NULL,
      '#default_value' => 'All',
    ];
    
  }
}

Ainsi les options des années disponibles seront calculées une première fois, puis récupérées ensuite directement depuis le cache de Drupal. Nous avons pris soin de rajouter un Cache Tag node:bulletin:year spécifique aux données stockées en cache afin de pouvoir les invalider juste quand cela sera nécessaire. En effet, nous avons mis en cache le résultat de nos options de façon permanente, et il nous faut juste invalider ces données si, et seulement si, un nouveau bulletin est créé ou modifié, et s'il contient une date dont l'année n'est pas présente dans les options mises en cache.

Invalidation du cache

Avec les caches tags, rien n'est plus simple pour un développeur Drupal 8. Regardons le snippet qui va se charger d'invalider les options mises en cache.

use \Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Cache\Cache;

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function my_module_node_presave(EntityInterface $entity) {
  $bundle = $entity->bundle();
  if ($bundle == 'bulletin') {
     // Check if a bulletin updated has a new year, and invalidate the
    // options cached used in the custom views filter for filtering by year.
    $cid = 'my_module:bulletin:year';
    $data = \Drupal::cache()->get($cid);
    if ($data) {
      $options = $data->data;
      $date = $entity->field_date->value;
      if ($date) {
        $date = new DrupalDateTime($date, new DateTimeZone('UTC'));
        $year = $date->format('Y');
        if (!isset($options[$year])) {
          Cache::invalidateTags(['node:bulletin:year']);
        }
      }
    }
  }
}

A chaque enregistrement d'un contenu de type bulletin, nous récupérons les options mises en cache ($data), puis comparons l'année de la date du contenu en cours d'enregistrement avec les valeurs en cache. En cas d'absence de l'année, grâce à la magie des Caches Tags,  alors il nous suffit simplement d'invalider le cache tag personnalisé (node:bulletin:year) que nous avons associé aux options mises en cache. Et au prochain chargement de la vue des bulletins, les options seront recalculées, mises à jour avec la nouvelle année, et à nouveau remises en cache pour une période indéterminée, et sans doute annuelle.

En guise de conclusion

Les arguments contextuels du module Views, intégré au coeur de Drupal 8, offrent un panel de possibilité considérable, pour couvrir des besoins les plus divers. Et quand ceux-ci ne suffisent pas, une simple implémentation d'un Plugin, en quelques lignes, nous permet d'insérer notre propre logique métier dans le mécanisme de Views de façon robuste et maintenable, conciliant ainsi la robustesse de Views et les spécificités d'un projet. Et au final pour un besoin apparement simple (filtrer par année), mais loin d'être aussi évident (travailler avec les dates est toujours source de surprise), ces derniers permettent de traiter le sujet de façon simple, sans devoir recourir à des moyens plus lourds. Et comme on dit, la simplicité est la mère de toutes les sûretés.

 

Commentaires

Soumis par JuanMvr (non vérifié) le 20/02/2018 à 12:07 - Permalien

Salut,

super article qui m'aide beaucoup à la découverte de Drupal 8

juste une question, si je veux ajouter un deuxième filtre avec les Mois

je duplique la fonction avec month ou je peux aussi ajouter dans la fonction existante les mois ?

Sinon super mine d'or ce blog ;)

Soumis par fabrice le 20/02/2018 à 16:32 - Permalien

Bonjour,

Merci pour le retour. Oui tout à fait vous pouvez aussi jouer sur les mois, même si ici cela risque de commencer à être gourmand côté requête SQL pour calculer les filtres. Le cache risquant d'être invalidé tous les mois. A voir pour votre cas, si utiliser les filtres à facettes avec search api n'est pas plus pertinent.

Soumis par Eric (non vérifié) le 03/03/2020 à 11:39 - Permalien

Salut,
avant tout j'aimerais te dire un grand merci pour cet article.
J'ai obtenu une erreur au niveau de l'initialisation du cache.
Avec un peu de recherche dans la documentation j'ai du remplacer CacheBackendInterface::CACHE_PERMANENT, par Cache::PERMANENT.

Ajouter un commentaire