Un site de menuiserie sur mesure recevait plusieurs milliers de demandes de devis par an, stockées dans un Custom Post Type devis. L’équipe commerciale avait besoin d’un écran d’administration dédié, distinct de la liste des articles, avec tri par montant, filtre par statut, et actions groupées pour marquer plusieurs devis comme traités d’un coup. C’est exactement le terrain de jeu de WP_List_Table.
Cette classe équipe déjà tous les tableaux natifs de l’admin — la liste des articles, des utilisateurs, des extensions — mais elle n’est officiellement pas documentée comme API publique stable. Il faut donc l’utiliser en connaissance de cause.
Étendre la classe et définir les colonnes
WP_List_Table n’est pas chargée par défaut : il faut explicitement inclure wp-admin/includes/class-wp-list-table.php avant de l’étendre, généralement depuis le callback de la page d’administration.
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
class Menuiserie_Devis_List_Table extends WP_List_Table {
public function get_columns() {
return array(
'cb' => '<input type="checkbox" />',
'client' => 'Client',
'montant' => 'Montant',
'statut' => 'Statut',
'date' => 'Date',
);
}
public function get_sortable_columns() {
return array(
'montant' => array( 'montant', false ),
'date' => array( 'date', true ),
);
}
}
La colonne cb, pour checkbox, est une convention du cœur : lui associer ce nom active automatiquement les cases à cocher nécessaires aux actions groupées, sans code supplémentaire.
Préparer les données avec prepare_items
La méthode prepare_items() concentre toute la logique métier : récupération des données, tri, pagination et affectation des colonnes visibles. C’est le cœur du fonctionnement de la classe.

public function prepare_items() {
$par_page = 20;
$page_courante = $this->get_pagenum();
$orderby = ! empty( $_GET['orderby'] ) ? sanitize_key( $_GET['orderby'] ) : 'date';
$order = ! empty( $_GET['order'] ) && 'asc' === $_GET['order'] ? 'ASC' : 'DESC';
$requete = new WP_Query( array(
'post_type' => 'devis',
'posts_per_page' => $par_page,
'paged' => $page_courante,
'orderby' => 'montant' === $orderby ? 'meta_value_num' : 'date',
'meta_key' => 'montant' === $orderby ? '_menuiserie_montant' : '',
'order' => $order,
) );
$this->items = array_map( 'menuiserie_formater_ligne_devis', $requete->posts );
$this->set_pagination_args( array(
'total_items' => $requete->found_posts,
'per_page' => $par_page,
) );
$this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
}
Oublier set_pagination_args() est une erreur discrète : le tableau continue de s’afficher, mais sans les liens de pagination en bas de page, ce qui devient rapidement un problème sur un catalogue de plusieurs milliers de lignes.
Actions groupées et rendu des colonnes
Les actions groupées demandent deux morceaux : la liste des actions disponibles, et leur traitement effectif, généralement réalisé au tout début de prepare_items() avant la requête principale.
protected function get_bulk_actions() {
return array(
'marquer_traite' => 'Marquer comme traité',
);
}
public function column_default( $item, $column_name ) {
return isset( $item[ $column_name ] ) ? esc_html( $item[ $column_name ] ) : '';
}
public function column_montant( $item ) {
return esc_html( number_format_i18n( $item['montant'], 2 ) ) . ' €';
}
Chaque colonne peut recevoir une méthode dédiée nommée column_{nom_de_colonne}, ce qui permet un rendu spécifique (mise en forme monétaire, badge coloré selon le statut) sans alourdir column_default.
Ce que WP_List_Table ne fait pas pour vous
La recherche et les filtres personnalisés (par statut, par plage de dates) ne sont pas gérés automatiquement : il faut les câbler à la main, généralement via des champs de formulaire au-dessus du tableau et une lecture de $_GET dans prepare_items(). La classe fournit seulement extra_tablenav() comme point d’accroche pour afficher ces contrôles au bon endroit visuellement.
- La recherche par mot-clé demande d’implémenter
$_REQUEST['s']soi-même dans la requête - Les filtres par statut se placent typiquement dans
extra_tablenav( $which ) - Aucune protection CSRF n’est fournie automatiquement pour les actions groupées : le nonce doit être vérifié manuellement dans le traitement
Traitez toujours
WP_List_Tablecomme un squelette, pas comme une solution clé en main : elle structure l’affichage, mais chaque comportement métier reste entièrement à votre charge.
Une classe privée, à utiliser en connaissance de cause
Le point le plus important à retenir, souvent absent des tutoriels : WP_List_Table est explicitement documentée par l’équipe cœur comme non destinée à un usage par des extensions tierces, sans garantie de stabilité de son API entre deux versions majeures de WordPress. Dans les faits, elle change rarement de façon incompatible, mais ce risque doit être assumé consciemment, en particulier pour une extension distribuée largement plutôt que développée sur mesure pour un seul client.
En résumé
Pour un écran d’administration interne, comme celui des devis de ce projet de menuiserie, WP_List_Table reste le choix le plus rapide et le plus cohérent avec le reste de l’interface WordPress. Elle impose son propre vocabulaire de méthodes à respecter, mais évite de réinventer pagination, tri et actions groupées à la main.