
Comment modifier et étendre les vues dans Drupal 10
Views est le constructeur de requêtes de Drupal. Il gère les listes de contenu, les résultats de recherche, les écrans d'administration et des dizaines d'autres tâches d'affichage sur la plupart des sites Drupal. Bien que l'interface d'administration couvre plusieurs cas d'utilisation, vous finirez par rencontrer des situations où vous aurez besoin d'un contrôle programmatique (filtrer des résultats selon une logique d'affaires complexe, ajouter des champs calculés ou modifier des requêtes d'une façon que l'interface ne supporte pas).
Ce guide couvre les principales techniques pour modifier et étendre Views dans Drupal 10 : les hooks du cycle de vie pour modifier les requêtes et les sorties, les plugins personnalisés pour des fonctionnalités réutilisables, et les hooks de données Views pour exposer des tables et champs personnalisés. Les approches présentées ici fonctionnent avec Drupal 8, 9, 10 et 11, puisque l'API Views est restée stable à travers ces versions.
Prérequis
Avant de travailler avec Views de façon programmatique, vous devriez avoir :
- Une installation Drupal 10 fonctionnelle avec Views activé (il fait partie du cœur)
- Un module personnalisé où placer votre code
- Une familiarité de base avec le système de hooks et l'architecture de plugins de Drupal
- Une compréhension du fonctionnement des requêtes Views (basées sur SQL par défaut)
- PHP 8.1 (requis pour Drupal 10)
Vous voudrez aussi avoir Drush installé pour vider les caches, puisque la découverte de Views est mise en cache de façon agressive. Après avoir ajouté de nouveaux hooks ou plugins, exécutez drush cr pour voir vos changements.
Comprendre le cycle de vie de Views
Views traite le contenu selon une séquence prévisible : construction, requête, exécution, rendu. Chaque phase a des hooks correspondants où vous pouvez intervenir :
Couche de configuration
- hook_views_data() - Déclarer des tables et champs à Views
- hook_views_data_alter() - Modifier les données Views d'autres modules
Couche de construction et requête
- hook_views_pre_view() - Modification précoce avant l'attachement de l'affichage
- hook_views_pre_build() - Après l'attachement des affichages, avant la construction de la requête
- hook_views_query_alter() - Modifier directement la requête de base de données
- hook_views_pre_execute() - Derniers changements avant l'exécution de la requête
Couche de résultats et rendu
- hook_views_post_execute() - Immédiatement après l'exécution de la requête
- hook_views_pre_render() - Avant la construction du tableau de rendu
- hook_views_post_render() - Modification finale de la sortie
Choisir le bon hook est important. Utilisez hook_views_query_alter() pour les changements au niveau SQL, hook_views_pre_render() pour la manipulation d'entités, et hook_views_post_render() pour les ajustements de balisage.
Étape 1 : Modifier les requêtes de vue
Le besoin programmatique le plus courant est de modifier la requête de base de données derrière une vue. hook_views_query_alter() vous donne un accès direct à l'objet de requête SQL.
Voici un exemple pratique qui restreint une vue pour n'afficher que le contenu publié d'un terme de vocabulaire spécifique :
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\query\Sql;
/**
* Implements hook_views_query_alter().
*/
function mymodule_views_query_alter(ViewExecutable $view, Sql $query) {
// Toujours vérifier les IDs de vue et d'affichage pour éviter d'affecter d'autres vues.
if ($view->id() === 'article_listing' && $view->current_display === 'page_1') {
// Ajouter une condition pour le contenu publié seulement.
$query->addWhere(0, 'node_field_data.status', 1, '=');
// Joindre à la taxonomie et filtrer par terme.
$definition = [
'table' => 'taxonomy_index',
'field' => 'nid',
'left_table' => 'node_field_data',
'left_field' => 'nid',
];
$query->addRelationship('taxonomy_index', $definition, 'node_field_data');
$query->addWhere(1, 'taxonomy_index.tid', 5, '=');
}
}Notre expérience démontre que l'erreur la plus fréquente ici est d'oublier de vérifier l'ID de la vue, ce qui fait que vos modifications affectent toutes les vues du site. Limitez toujours vos changements de façon précise en utilisant à la fois $view->id() et $view->current_display.
L'objet $query fournit plusieurs méthodes utiles :
- addWhere($group, $field, $value, $operator) - Ajouter des conditions
- addRelationship($alias, $join, $base) - Ajouter des jointures de tables
- addOrderBy($table, $field, $order) - Ajouter un tri
- addGroupBy($field) - Ajouter un regroupement
Pour un filtrage contextuel, vous pouvez accéder à l'utilisateur courant, aux paramètres de requête, ou à tout service injecté dans le hook :
function mymodule_views_query_alter(ViewExecutable $view, Sql $query) {
if ($view->id() === 'user_content') {
$current_user = \Drupal::currentUser();
if (!$current_user->hasPermission('view all content')) {
// Restreindre au contenu de l'utilisateur.
$query->addWhere(0, 'node_field_data.uid', $current_user->id(), '=');
}
}
}Étape 2 : Modifier les résultats et le rendu
Quand vous devez changer ce que les utilisateurs voient sans modifier la requête sous-jacente, utilisez les hooks de résultats et de rendu.
Manipulation post-exécution
hook_views_post_execute() s'exécute immédiatement après la requête. Vous pouvez modifier $view->result directement :
use Drupal\views\ViewExecutable;
/**
* Implements hook_views_post_execute().
*/
function mymodule_views_post_execute(ViewExecutable $view) {
if ($view->id() === 'featured_content') {
// Ajouter des données calculées à chaque ligne de résultat.
foreach ($view->result as $index => $row) {
$row->custom_score = calculate_relevance_score($row->_entity);
}
}
}Manipulation d'entités pré-rendu
hook_views_pre_render() est utile pour modifier les entités avant qu'elles ne deviennent du HTML :
/**
* Implements hook_views_pre_render().
*/
function mymodule_views_pre_render(ViewExecutable $view) {
if ($view->id() === 'article_listing') {
foreach ($view->result as $row) {
if (!empty($row->_entity) && $row->_entity->bundle() === 'article') {
// Marquer les articles en vedette dans le titre.
if ($row->_entity->get('field_featured')->value) {
$current_title = $row->_entity->label();
$row->_entity->setTitle($current_title . ' ★');
}
}
}
}
}Changements de sortie post-rendu
Pour les ajustements finaux de balisage, utilisez hook_views_post_render() :
/**
* Implements hook_views_post_render().
*/
function mymodule_views_post_render(ViewExecutable $view, array &$output, &$cache) {
if ($view->id() === 'product_grid') {
$output['#prefix'] = '<div class="product-grid-wrapper">';
$output['#suffix'] = '</div>';
$output['#attached']['library'][] = 'mymodule/product-grid';
}
}Étape 3 : Créer des plugins de champs personnalisés
Pour des fonctionnalités de champs réutilisables, créez un plugin de champ Views personnalisé. Ceux-ci se trouvent dans src/Plugin/views/field/ à l'intérieur de votre module.
<?php
namespace Drupal\mymodule\Plugin\views\field;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;
/**
* A custom Views field that displays entity status and freshness.
*
* @ViewsField("entity_status_freshness")
*/
class EntityStatusFreshness extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function query() {
// Leave empty to avoid altering the query.
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$entity = $this->getEntity($values);
if (!$entity) {
return '';
}
$status = $entity->isPublished() ? 'published' : 'draft';
$age = time() - $entity->getCreatedTime();
if ($age < 86400) {
$freshness = 'new';
}
elseif ($age < 604800) {
$freshness = 'recent';
}
else {
$freshness = 'archived';
}
return [
'#markup' => "<span class=\"status-{$status} freshness-{$freshness}\">"
. $this->t('@status / @fresh', [
'@status' => ucfirst($status),
'@fresh' => ucfirst($freshness),
])
. "</span>",
];
}
}Pour rendre ce champ disponible dans Views, enregistrez-le via hook_views_data_alter() :
/**
* Implements hook_views_data_alter().
*/
function mymodule_views_data_alter(array &$data) {
$data['node_field_data']['content_status_indicator'] = [
'title' => t('Indicateur de statut du contenu'),
'help' => t('Affiche le statut de publication et la fraîcheur du contenu.'),
'field' => [
'id' => 'content_status_indicator',
],
];
}Après avoir vidé le cache, ce champ apparaît dans l'interface Views lors de la construction de vues basées sur le contenu.
Étape 4 : Créer des plugins de filtres personnalisés
Les filtres personnalisés vous permettent d'ajouter une logique de filtrage que l'interface standard ne supporte pas. Voici un filtre qui restreint le contenu selon les relations de rôles utilisateur :
<?php
namespace Drupal\mymodule\Plugin\views\filter;
use Drupal\views\Plugin\views\filter\FilterPluginBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Custom filter for role-based content access.
*
* @ViewsFilter("role_based_content_filter")
*/
class RoleBasedContentFilter extends FilterPluginBase {
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['moderation_level'] = ['default' => 'own'];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$form['moderation_level'] = [
'#type' => 'select',
'#title' => $this->t('Moderation scope'),
'#options' => [
'own' => $this->t('Own content only'),
'department' => $this->t('Department content'),
'all' => $this->t('All content'),
],
'#default_value' => $this->options['moderation_level'],
];
}
/**
* {@inheritdoc}
*/
public function query() {
$this->ensureMyTable();
$current_user = \Drupal::currentUser();
switch ($this->options['moderation_level']) {
case 'own':
$this->query->addWhere(
$this->options['group'],
'node_field_data.uid',
$current_user->id(),
'='
);
break;
case 'department':
// Get department IDs for current user.
$departments = $this->getUserDepartments($current_user->id());
if (!empty($departments)) {
$this->query->addWhere(
$this->options['group'],
'node__field_department.field_department_target_id',
$departments,
'IN'
);
}
break;
case 'all':
// No filtering.
break;
}
}
/**
* Gets department IDs for a user.
*/
protected function getUserDepartments($uid) {
// Implementation depends on your site structure.
return [];
}
}Enregistrez le filtre de façon similaire au plugin de champ :
function mymodule_views_data_alter(array &$data) {
$data['node_field_data']['user_moderation_scope'] = [
'title' => t('Portée de modération utilisateur'),
'help' => t('Filtre le contenu selon les permissions de modération de l\'utilisateur.'),
'filter' => [
'id' => 'user_moderation_scope',
],
];
}Étape 5 : Exposer des données personnalisées à Views
Pour rendre des tables de base de données personnalisées disponibles dans Views, implémentez hook_views_data() :
/**
* Implements hook_views_data().
*/
function mymodule_views_data() {
$data = [];
$data['mymodule_metrics'] = [
'table' => [
'group' => t('Métriques de contenu'),
'base' => [
'field' => 'id',
'title' => t('Métriques de contenu'),
'help' => t('Données de métriques personnalisées pour le contenu.'),
],
// Jointure aux nodes.
'join' => [
'node_field_data' => [
'left_field' => 'nid',
'field' => 'entity_id',
],
],
],
'id' => [
'title' => t('ID de métrique'),
'field' => ['id' => 'numeric'],
'filter' => ['id' => 'numeric'],
'sort' => ['id' => 'standard'],
],
'view_count' => [
'title' => t('Nombre de vues'),
'field' => ['id' => 'numeric'],
'filter' => ['id' => 'numeric'],
'sort' => ['id' => 'standard'],
],
'engagement_score' => [
'title' => t('Score d\'engagement'),
'field' => ['id' => 'numeric'],
'filter' => ['id' => 'numeric'],
'sort' => ['id' => 'standard'],
],
];
return $data;
}Cela rend les champs de votre table personnalisée disponibles comme champs, filtres et critères de tri Views, avec une jointure automatique aux nodes.
Erreurs courantes à éviter
Ne pas limiter les hooks à des vues spécifiques : Chaque hook devrait vérifier $view->id() et habituellement $view->current_display avant de faire des changements. Les hooks non limités affectent toutes les vues de votre site.
Oublier de vider les caches : La découverte des plugins Views et l'enregistrement des hooks sont mis en cache. Exécutez drush cr après avoir ajouté ou modifié du code Views.
Mauvais namespace ou annotation : Les plugins doivent se trouver dans le bon répertoire (src/Plugin/views/field/, src/Plugin/views/filter/, etc.) avec les annotations correspondantes. Un fichier mal placé ne sera pas découvert.
Traitement lourd dans les hooks de rendu : Nous recommandons de garder les hooks de phase de rendu légers. Déplacez la logique d'affaires complexe dans des services que vous appelez depuis les hooks plutôt que de tout implémenter directement.
Ignorer la performance des requêtes : Ajouter plusieurs jointures ou conditions complexes dans hook_views_query_alter() peut ralentir significativement le chargement des pages sur de grands ensembles de données. Testez avec des données de taille production et vérifiez la performance des requêtes.
Modifier les entités directement : Dans hook_views_pre_render(), les changements à $row->_entity persistent dans le cache. Si vous modifiez des propriétés d'entités pour l'affichage, considérez si ces changements pourraient apparaître ailleurs de façon inattendue.
Tests et vérification
Après avoir implémenté des modifications Views, vérifiez qu'elles fonctionnent correctement :
Vérifier l'exécution des hooks
Ajoutez une journalisation temporaire pour confirmer que vos hooks s'exécutent :
function mymodule_views_query_alter(ViewExecutable $view, Sql $query) {
\Drupal::logger('mymodule')->notice('Query alter déclenché pour @view', [
'@view' => $view->id(),
]);
// Votre implémentation réelle...
}Inspecter la requête générée
Activez le module Devel et utilisez dpq() pour voir le SQL :
function mymodule_views_query_alter(ViewExecutable $view, Sql $query) {
if ($view->id() === 'my_view') {
// Après vos modifications.
dpq($query);
}
}Tester la découverte des plugins
Vérifiez que vos plugins personnalisés apparaissent dans l'interface Views. S'ils n'apparaissent pas :
- Vérifiez que le fichier est dans le bon répertoire src/Plugin/views/[type]/
- Vérifiez que le namespace correspond au chemin du fichier
- Confirmez que l'annotation est correcte (@ViewsField, @ViewsFilter, etc.)
- Videz tous les caches
Vérifier avec différents utilisateurs
Si vos modifications impliquent des permissions ou un contexte utilisateur, testez avec des comptes ayant différents rôles pour confirmer que la logique fonctionne correctement à travers les niveaux de permissions.
Vérifier le comportement de mise en cache
Views met en cache les résultats de façon agressive. Testez que vos modifications dynamiques fonctionnent correctement quand le cache est activé, ou configurez les contextes/tags de cache appropriés si les résultats varient selon l'utilisateur ou le contexte.
Conclusion
Le contrôle programmatique de Views dans Drupal 10 se résume à choisir le bon point d'intervention. Utilisez hook_views_query_alter() pour le filtrage au niveau base de données, les hooks de résultats et de rendu pour les modifications d'affichage, et les plugins personnalisés pour les fonctionnalités réutilisables. L'API Views est restée stable de Drupal 8 à 11, donc ces techniques vous serviront bien pendant des années.
Travailler avec des équipes nous a appris que les meilleures personnalisations Views sont celles qui restent ciblées et bien documentées. Un seul hook faisant une chose clairement vaut mieux qu'un hook complexe essayant de gérer plusieurs scénarios. Quand vos besoins dépassent ce que les hooks peuvent gérer élégamment, les plugins personnalisés avec injection de dépendances appropriée vous donnent du code testable et maintenable.
Si vous construisez des flux de travail éditoriaux complexes ou avez besoin que Views gère une logique d'affaires au-delà de ce que l'interface supporte, avoir un plan clair pour l'emplacement de chaque élément de logique fait une différence significative. Nous travaillons avec des équipes pour concevoir des architectures Views qui répondent à leurs besoins spécifiques en matière de contenu sans créer de maux de tête de maintenance à long terme. Contactez-nous si vous souhaitez discuter de votre approche.
