Tableau d'adminidtration WorPress : Guide WP_List_Table

Tableau WordPress intégré et WP_List_Table : guide d’utilisation détaillé

Créer un tableau WordPress customisé, paginé et « responsive », pour la consultation des posts, pages ou utilisateurs, et l’exécution d’actions groupées (ou individuelles), peut se révéler fastidieux. La classe WP_List_Table facilite la tâche du webmaster ou développeur web, en proposant des méthodes à surcharger, à la manière d’un framework, et un processus d’intégration simplifié.

Avant de se précipiter dans le code, il peut être bon de replacer cette classe dans son contexte afin d’éviter quelques désagréments et utiliser l’outil de manière efficace dans construction d’un tableau WordPress. Cet article est basé sur la référence de la classe WP_List_Table du codex, l’article de Agbonghama Collins Using WP_List_Table to Create WordPress Admin Tables et le plugin Custom List Table Example développé par Matt van Andel.

Rôle de la class WP_List_Table

Le rôle de WP_Liste_Table est de générer les tableaux de listes qui peuplent les différents écrans d’administration de WordPress. Son avantage sur les précédentes versions est de pouvoir être altéré dynamiquement avec AJAX.

Dans le core de WordPress

Le core de WP charge et retourne dynamiquement les classes étendant WP_List_Table (comme ‘WP_Posts_List_Table’, ‘WP_Media_List_Table’, ‘WP_Terms_List_Table’, ‘WP_Users_List_Table’, ‘WP_Comments_List_Table’, etc.) en utilisant _get_list_table(), notamment dans wp-admin/includes/theme-install.php. Cette fonction charge automatiquement les classes appropriées et les instancie.

Utilisation des développeurs et statut privé

Le codex de WordPress nous inique :

Le 27 mars 2012, Andre Nacin avait prévenu les développeurs que la classe, pouvant être sujette à modifications, avait été prévue pour être utilisée de façon privée par le core de WordPress, et non pas telle quelle par les développeurs. Néamoins, fournissant un moyen fiable, consistant et sémantique de produire des tableaux de liste customisés, elle est devenue largement utilisée par les développeurs et les plugins. Jusqu’ici aucune modification majeure ne lui ayant été apporté, tester un plugin l’utilisant avec une version « Beta/RC » de WordPress devrait être suffisant pour éviter des problèmes majeurs. Un autre moyen commun de travailler avec cette classe est d’en faire une copie (/wp-admin/includes/class-wp-list-table.php) à distribuer au sein du plugin, limitant les risques dus aux mises à jour. Les développeurs devraient donc être conscients que son utilisation est à leurs risques et périls.

OK, nous somme prévenus 😯 !

Workflow

Une classe étendant WP_List_Table doit être créée pour surcharger les méthodes clés de la classe parente hydratant les cellules du tableau WordPress. Elle doit être ensuite instanciée (généralement au niveau du formulaire) pour permettre un appel aux méthodes prepare_item() et display() qui initialisent et affichent le tableau. Rien ne valant un bon exemple, nous allons construire un plugin. Mais il est important de connaître l’outil avec lequel nous travaillons.

WP_List_Table : le framework

Rappelons donc en préambule que cette classe produit un balisage de type tableau (<table>), typiquement chargé d’éléments issus de la base de données. Chaque ligne du tableau devrait donc correspondre à une ligne de résultat de la requête adressée à la BDD, un item. On se réfère à cet item à travers la propriété $item des méthodes de la classe (telles que column_default( $item, $column_name ), columns_{ma_colonne}( $item ) ou column_cb( $item ) ) chargées d’alimenter chaque colonne de chaque ligne (item) renvoyée. Cette propriété est elle-même un tableau associatif dont les clés devraient être issues de la requête et dotées des valeurs correspondantes dans les tables de la BDD. En clair, si vous récupérer la colonne ‘title’ d’un « Custom Post Type » en base de données, vous aurez accès à $item[ ‘title’ ] au sein de ces méthodes.

Une classe à étendre

La classe WP_List_Table doit être étendue pour fonctionner, nous allons donc créer notre classe enfant. Les méthodes suivantes doivent impérativement être surchargées pour que la classe fonctionne :

__construct() : pour initialiser quelques valeurs par défaut et surtout transmettre au constructeur parent les formes singulières et plurielles de ce que l’on souhaite afficher, ainsi qu’une variable destinée à l’utilisation d’AJAX pour le tableau. À titre d’exemple, pour un affichage de posts pourra être celui-là :


parent::__construct([ 'singular' => 'post', 'plural' => 'posts', 'ajax'	=> false ]);

Ces formes nominatives seront stockées dans la propriété $_args de WP__List_Table.

column_{column_name}( $item ), où {column_name} est typiquement le nom d’une colonne issue de la table de données, est la méthode permettant de définir quelle colonne du tableau restera visible sur les mobiles. Les autres étant cachées par défaut pour les petits supports, le tableau devient tableau « responsive ». Cette fonction nous permet en plus d’insérer dans cette colonne « primaire » les liens vers des actions individualisées pour chaque ligne.

row_actions() n’est pas à surcharger, mais elle est appelée depuis column_{column_name}( $item ) et chargée de retourner un bloc HTML formaté contenant les liens vers les traitements individuels de chaque ligne( suppression ou autre).

column_default( $item, $column_name ) est une méthode permettant de définir un type de sotie standardisé pour les contenus de chaque colonne à l’exception de la colonne primaire. Elle est appelée pour chaque colonne de chaque ligne récupérée de la base de données. On ciblera donc les éléments souhaités avec $item[ $column_name ].

column_cb( $item ), comme on s’y attend, sert à définir un input de type checkbox multiple, pour la colonne des actions « bulk ». Le nom du champ sera récupéré depuis la propriété $_args[ ‘singular’ ] de la classe (celui-là même que nous avons défini dans le constructeur), et sa valeur sera l’ID de la ligne affichée $item[ ‘ID’ ].

get_columns() nous permet de retourner un tableau contenant chaque colonne issue de la base de données sous la forme « cle_base_de_donnée » => « Titre », définissant les en-têtes des colonnes à insérer.

get_sortable_columns() nous sert à définir les colonnes triables.

Du fait qu’il est possible d’ « aliaser » les noms de colonnes dans la requête, on retournera ici un tableau contenant pour chaque colonne triable, une clé « identifiant_colonne » (« nom ou alias) et une valeur sous forme de tableau spécifiant si la colonne est triée depuis la requête : « identifiant  » => array( « nom_colonne_base_données » => true ).

get_bulk_actions() est la fonction définissant et retournant les actions groupées. Elle doit être écrite par nos soins puisque ces actions sont spécifiques à chaque utilisation.

process_action() n’est pas prédéfinie, mais nous l’implémentons si nous souhaitons exécuter des actions groupées et individuelles pour traiter nos éléments. Nous l’appellerons depuis prepare_items(), avant toute récupération de données pour effectuer les éventuelles modifications sur la base de données.

prepare_items() est sans doute la méthode la plus importante de la classe. On s’en sert pour effectuer les potentielles actions, récupérer les data en base de données, récupérer les colonnes, colonnes triables et colonnes cachées pour hydrater les en-têtes du tableau, préparer (hydrater) les lignes du tableau de sortie et transmettre les éléments de pagination (nombre total d’enregistrements en base de données, nombre d’items par page, et nombre de pages).

set_pagination_args() est chargée de définir les éléments utiles aux 2 modules de pagination (haut et bas).

display() est la méthode servant à afficher le balisage complet du tableau hydraté et des modules de navigations. Normalement nous n’avons pas à y toucher. Cependant, un regard rapide à cette méthode mère nous donne quelques précieux renseignements :

  • Seul le balisage <table> est retourné, nous forçant à nous occuper nous-mêmes du formulaire sur la page d’administration.
  • La navigation est appelée deux fois avec $this->display_table_nav( $where ). L’argument attend « top » ou « bottom ».
  • Les en-têtes sont aussi appelées 2 fois fois avec $this->print_column_headers() et retournent les cellules <th> dans les zones <thead> et <tfoot> du tableau.
  • Les lignes sont appelées dans le <tbody>; avec $this->display_rows_or_placeholder()

Ces renseignements seront précieux lorsque nous voudrons traiter nous actions avec AJAX, et nous pourrons surcharger cette méthode display() pour y insérer nos éléments de sécurité et de pagination.

Quelques fonctions membres utiles

pagination( $which ) créée et retourne la pagination HTML affichée à droite de l’en-tête et du pied de page du tableau WorPress. On n’a généralement pas besoin de l’utiliser directement, elle est prise en charge par la méthode display(), mais nous verrons que nous pouvons l’utiliser pour la prise en charge AJAX (voir le prochain article).

ajax_response() nous permettra de reconstruire nos parties de tableau avec AJAX.

ajax_user_can() est l’endroit ou nous définissons des permissions spécifiques aux données demandées pour un traitement AJAX (nous ne la verrons que lors du prochain article sur l’implémentation d’AJAX sur notre classe étendue).

print_column_headers() retourne toutes les cellules (et leur contenu) des lignes de l’en-tête <thead> et du pied de page <tfoot>.

Schéma d’affichage du corps du tableau.

Il est bon de comprendre le processus de génération des lignes et colonnes de notre tableau pour appréhender plus simplement les méthodes à surcharger :

display_rows_or_placeholder() retourne un balisage <tr> unique comportant une cellule unique et un message si aucun élément n’est trouvé (no_items() sert à personnaliser ce message). Si au contraire, des résultats sont trouvés, les méthodes suivantes sont appelées en cascade :

  • display_rows() est chargée de l’itération sur les items).
  • single_row( $item ) injecte le balisage <tr> de chaque résultat.
  • single_row_columns( $item ) est chargé de l’itération sur les colonnes de chaque item, et du balisage des cellules <th> et <td> avec attributs.
  • _column_{column_name}( $item ) est une méthode dynamique affichant la colonne primaire
  • handle_row_actions( $item, $column_name, $primary ) est chargée de retourner un bouton toggle permettant l’affichage des colonnes non primaires sur les mobiles.

OK, nous avons brièvement parcouru nos 12 méthodes principales, cela paraît un peu compliqué, mais un peu de pratique va nous permettre de démystifier le machin ;-).

Les propriétés importantes de WP_List_Table

Pour tenter de faire court, on ne passe en revue que les propriétés directement et nécessairement utilisables depuis nos méthodes.

  • $_column_headers reçoit les noms de colonnes, colonnes cachées et colonnes triables du tableau.
  • $items devra contenir un tableau présentant les lignes d’enregistrements récupérées depuis la base de données.
  • $_pagination_args contient tous les paramètres de pagination définis dans prepare_items().

1 item, 1 enregistrement en base de données

Les méthodes à surcharger utilisent pour partie l’argument $item. Il représente une ligne complète de résultat issu de la base de données présenté sous forme de tableau associatif. Son contenu dépend donc de notre propre requête initiale, mais à chaque fois que vous le rencontrez, vous y trouvez toutes les colonnes du résultat.

Instanciation de la classe enfant

L’instanciation ne notre classe enfant se fera au niveau de la page d’administration qui en aura besoin. D’une part parce que c’est logique, en plus parce que cela laisse le temps à WP_Screen d’être accessible (le constructeur de la classe parente utilise la méthode convert_to_screen() définie dans le fichier wp-admin/includes/template.php. Ce fichier fait lui-même appel à la méthode get() de la class WP_Screen … cette info n’est pas vitale … ).

Nous allons récupérer l’instance au niveau du formulaire (ainsi que sur le callback AJAX servant à renvoyer les éléments mis à jour, mais c’est pour l’article suivant). Nous allons donc créer un singleton chargé d’invoquer le constructeur et de nous renvoyer une instance unique.

La classe WP_List_Table et tableau WordPress par l’exemple

Pour les besoins de cet article, nous allons donc créer un plugin et construire un tableau présentant les posts enregistrés en brouillon. Après tout, vous pourriez très bien avoir à disposition une armée d’auteurs hyper productifs, quelques dizaines d’articles en attente et le besoin de les gérer de façon centralisée … Et puis il me fallait un exemple standard … 😉 Nous allons créer un tableau WordPress présentant une liste paginée des brouillons sur une page d’administration, avec leur titre, auteur et date.

Un constructeur destiné à la classe parente.

Commençons par l’initialisation du plugin.


<?php
/**
 * Plugin name: JST Simple List Plg;
 * Description: SImple plugin using WP_List_Table
 */
if( !defined(  'ABSPATH' ) ) exit;
if( !class_exists( 'WP_List_Table' ) )
{
	if( file_exists( ABSPATH.'/wp-admin/includes/class-wp-list-tabe.php' ) ){
		require_once( ABSPATH.'/wp-admin/includes/class-wp-list-tabe.php' );
	}else
	{
		require_once( ABSPATH.'/wp-admin/includes/class-jst-list-tabe.php' )
	}
}	
class JST_List_Table extends WP_List_Table {

Nous utilisons le constructeur pour définir un tableau de propriétés comportant le nom de l’entité à afficher au singulier, sa forme plurielle, et une valeur booléenne autorisant l’utilisation de AJAX pour la table Ces éléments sont ensuite passés au constructeur parent pour une fusion avec un tableau défini par défaut, et leur assignation à la propriété $_args. On profite de la classe pour définir et stocker un certain nombre de propriétés utiles :


class JST_Table
{	
	private $posttablename 		= 'posts';
	private $userstablename		= 'users'; 
	public static $instance		 = null;
	private $tablename		= 'posts';
	private $perpage		= 4;
	private $deletenonceaction	= '_jst_detele_post';
	private $deletenoncename 	= 'jstdelnce';
	private $editnonceaction 	= '_jst_edit_post';
	private $editnoncename 		= 'jstedtnce';
	private $publishnonceaction	= '_jst_publish_post';
	private $publishnoncename	= 'jstpubnce'; 

	public $foundrows;

	public function __construct()
	{
		parent::__construct( [
			'singular'	=> 'post',
			'plural'	=> 'posts',
			'ajax'		=> true
		] );
	}

OK, il est temps de définir notre tableau.

Définition de la colonne primaire : Pour un tableau responsive et des actions individuelles

WP_List_Table met à notre disposition une méthode générique column_{nom_colonne}( $item, $column_name ) servant 2 objectifs. D’une part elle définit la « colonne primaire » (et sa valeur). Elle restera la seule cette colonne visible au chargement de la page sur les « viewports » les plus petits. De plus, elle insère sous la valeur de la colonne les liens vers les actions individuelles disponibles pour chaque ligne de résultats (« delete », « edit » ou ce que vous voudrez). Survolez la colonne pour en voir les effets.

« column_ » est ici le préfixe de la méthode. {nom_colonne} correspond à la colonne choisie par nos soins dans la structure de notre base de données. À titre d’exemple, si vous souhaitez basiquement garder accessible la colonne des titres sur les mobiles, vous créerez une méthode column_title( $item );

L’argument $item est le tableau associatif reprenant l’enregistrement complet de chaque ligne en BDD de tel qu’il a été formaté dans votre requête SQL.

Pour le rendu des liens, nous passons par la méthode row_actions() de la classe parente. Elle attend un tableau d’actions de la forme « nom_action » => lien (un lien pour chaque action), nous devrons donc construire nous-mêmes ce tableau et ces liens, et retourner une chaîne comportant la valeur de la colonne et les liens formatés (on écrit tout ça juste après).

Les liens d’actions vers la page d’administration.

Pour la construction des liens vers une page d’administration WordPress, on utilise le paramètre « page » de son URL.

À notre niveau, ce paramètre sera défini directement en 4e argument de la fonction WordPress add_menu_page(). Il reprendra généralement le nom même du fichier du plugin (basename( __FILE__)).

Pour pointer vers notre fichier d’administration on récupère donc l’argument « page » depuis $_REQUEST.

On fournira au paramètre d’URL « post » l’identifiant (« ID ») de l’item ciblé (correspondant à la ligne de résultat de la BDD affichée sur la ligne du tableau) permettant le traitement SQL ultérieur de la ligne (« delete », « edit » ou autre).

Enfin on injectera le nom de l’action à réaliser, ça, c’est facile puisque nous la définissons nous-mêmes. Le paramètre de requête « action », s’il est présent, sera renvoyé par la méthode current_action(), dont nous nous servirons pour l’exécuter de façon ciblée au sein de notre méthode process_action() (nous voyons cela plus loin).

Pour les besoins de l’article, nous créons 3 liens vers des actions individuelles permettant l’édition, la publication et la suppression des posts. Nous en profitons pour sécuriser nos liens. On y va :


public function column_title( $item )
{
	// Publish
	$publishnonceaction = $this->publishnonceaction;
	$publish_url = add_query_arg([
		'page'		=> $_REQUEST[ 'page' ],
		'post'		=> $item[ 'ID' ],
		'action'	=> 'publish'
	]);
	$nonced_publish_url = wp_nonce_url( $publish_url, $publishnonceaction, $this->publishnoncename );

	// Edit
	$proto = isset( $_SERVER[ "HTTPS" ] ) ? 'https' : 'http';
	$edit_url = admin_url( 'post.php', $proto );
	$edit_url = add_query_arg( [
		'post'		=> $item[ 'ID' ],
		'action'	=> 'edit'
	], $edit_url );

	// Delete 
	$deletenonceaction = $this->deletenonceaction;
	$delete_url = add_query_arg( [
		'page'		=> esc_url( $_REQUEST[ 'page' ] ),
		'action'	=> 'delete',
		'post'		=> $item[ 'ID' ]
	] );
	$nonced_delete_url = wp_nonce_url( $delete_url, $deletenonceaction, $this->deletenoncename );

	$actions = [
		'edit'		=> sprintf(
			'<a href="%1$s">%2$s</a>',
			$edit_url,
			__( 'Edit', 'jst' )
		),
		'delete'	=> sprintf(
			'<a href="%1$s">%2$s</a>',
			$nonced_delete_url,
			__( 'Delete', 'jst' )
		),
		'publish'	=> sprintf(
			'<a href="%1$s">%2$s</a>',
			$nonced_publish_url,
			__( 'Publish', 'jst' )
		)
	];
	return sprintf( "%s %s", $item[ 'title' ], $this->row_actions( $actions ) );
}

Notez que nous retournons finalement une chaîne contenant en même temps la valeur de la colonne primaire pour la ligne et les liens-actions :


return sprintf( %1$s %2$s, $item[ 'name' ], $this->row_action( $actions ) );

La méthode de classe row_action() formate le HTML pour les liens et inclut un bouton « toggle » pour la ligne, autorisant l’affichage du reste des colonnes disponibles pour les viewports de petite dimension.

Le framework nous fournit une méthode générique pour l’affichage des autres colonnes.

L’affichage par défaut des colonnes

column_default() retourne la valeur de chaque colonne de façon standardisée (si aucune méthode spécifique n’a été déclarée pour cette colonne). Elle attend les arguments $item (ligne complète issue de la BDD) et $column_name (la colonne à traiter). Elle ne devrait renvoyer qu’une valeur et uniquement pour les champs attendus. On passe donc en revue ces champs attendus et pour retourner retourne simplement la valeur correspondante. Libre à vous de formater vos cellules comme bon vous semble :


public function column_default( $item, $column )
{
	switch( $column )
	{
		case "date":
		case 'user':
			return $item[ $column ];
			break;
		default:
			return print_r( $item, 1 );
	}
} 

Cas de la colonne de la checkbox : les actions groupées

La méthode single_row_columns( $item ) de la classe mère, génère les colonnes pour une ligne unique, et recherche, en premier lieu, la présence d’une colonne dont l’identifiant est « cb » dans le tableau $columns (récupéré via $this->get_columns_info()). Cette colonne sert à afficher un champ input du type checkbox pour chaque ligne, pour la prise en charge d’une action groupée (« bulk action »).

Lorsque cette colonne est repérée, single_row_columns() appelle $this->column_cb( $item ) pour afficher ce champ. La méthode column_cb() de la classe mère étant vide, nous devons la surcharger si nous souhaitons afficher une checkbox d’actions groupées. Nous allons retourner un champ input formaté, dont l’attribut « name » correspondra au nom de l’entité au singulier stocké dans $_args (voir le constructeur), suivi de « [] » (pour la prise en compte de valeurs multiples). L’attribut value recevra l’ID correspondant à la ligne :


public function column_cb( $item ){
	return sprintf(
		'<input type="checkbox" name="%1$s[]" value="%2$s"/>',
		$this->_args[ "singular" ],
		$item[ "ID" ]
	);
}

OK, nous avons maintenant identifié les méthodes d’hydratation et de rendu des lignes du tableau. Il nous faut maintenant définir quelles colonnes sont attendues ainsi que les colonnes triables.

Déclaration des colonnes, et colonnes triables

Le tableau WordPress final comportant des éléments que seul le développeur connaît à l’avance, il faut déclarer les colonnes directement dans les getters appropriés. get_columns() permet de déclarer toutes les colonnes attendues (identifiants et libellés). Notons la présence d’une colonne ‘cb’ réservée à la checkbox des actions groupées.


public function get_columns()
{
	return [
		'cb'	=> '<input type="checkbox" />',
		'title' => __( 'Title', 'jst' ),
		'user'	=> __( 'User', 'jst' ),
		'date'	=> __( 'Date', 'jst' )
	];
}

Rendre les colonnes triables

get_sortable_columns() doit retourner un tableau associatif dont les clés sont les colonnes triables, et la valeur, un tableau à 2 éléments : la colonne des data en base de données, et un booléen indiquant si les données sont déjà filtrées.


public function get_sortable_columns()
{
	return [
		'title'		=> array( 'post_title', true ),
		'user'		=> array( 'user_nicename', false ),
		'date'		=> array( 'post_date', false )
	];
} 

Définition des colonnes cachées et déclaration des en-têtes du tableau

La classe attend la définition des trois tableaux $columns, $hidden et $sortable pour hydrater la propriété $this->_column_headers. On utilisera dans la méthode prepare_items() les 2 getters vus précédemment et on assignera à $hidden un tableau des colonnes à cacher de notre choix au même endroit. Ce tableau ne comporte que les noms de colonnes ou peut rester vide :


$columns = $this->get_columns()
$hidden = array();
$sortable = $this->get_sortable_columns();
$this->_column_headers = array( $columns, $hidden, $sortable );

Nous reviendrons un peu plus loin sur la préparation du tableau avec prepare_items().

Définition et traitement des actions groupées : « bulk actions »

Les actions groupées dépendent de votre objectif et doivent être définies et retournées sous forme de tableau par la méthode get_bulk_actions() (par défaut la méthode de la classe parente retourne aussi un tableau vide).


protected function get_bulk_actions(){
	return [
		'publish'	=> __( 'Publish', 'jst' ),
		'delete' 	=> __( 'Delete', 'jst' ),
	];
}

Là encore, les actions groupées dépendent de l’utilisation du tableau. Dans notre cas nous proposons la publication ou la suppression des posts sélectionnés.

Le traitement des actions

Nous créons notre propre méthode process_action() pour exécuter l’action courante.

Petit aparté : Contrairement à Matt van Andel, je n’appelle volontairement pas cette méthode process_bulk_action(), puisque current_action() (class mère) renvoie globalement la valeur de $_REQUEST[ « action » ] (ou $_REQUEST[ « action2 » ] pour les actions « bulk » inférieures), qu’il soit passé par le formulaire «bulk» ou au sein des URL des « row_action ». Nous l’utiliserons donc aussi pour traiter l’ensemble des actions disponibles.

Un schéma de traitement

Encore une fois je vous propose le mien. À vous d’implémenter le processus qui convient à vos données (c’est l’esprit framwork 😉 ). Regardons ce dont nous disposons :

Pour rappel :

L’attribut «name» des checkbox de la colonne «bulk» est construit avec le paramètre «singular» (passé dans le constructeur de classe). Dans notre cas : «post».

Les liens vers les actions individuelles possèdent un paramètre du même nom («post») ayant pour valeur associée l’ID de l’enregistrement en base de données (la ligne ciblée).

La valeur récupérée depuis $_REQUEST[ ‘post’ ] sera donc un entier ou un tableau d’entiers.

Nos liens actions sont dotés d’un paramètre « action ». « Action » est aussi le nom des 2 champs <select> pour les actions groupées. Nous récupérons la valeur associée à « action » avec la méthode current_action(). Notez qu’en fait, le champ select inférieur est nommé « action2 » et que current_action() est justement chargée retourner la valeur associée à l’un ou l’autre des <sélect>.

Le tableau possède un champ , un « nonce_field » de nom « _wpnonce » créé par la méthode display_tablenav() et cryptant « bulk-« .$this->_args[ « plural » ]. Nous nous en servirons pour sécuriser notre tableau WordPress.

Nous allons donc :

  • Vérifier la présence et le type des paramètres « post » et « action » dans la requête HTTP.
  • Vérifier les nonces en fonction du type de donnée reçu (entier ou tableau)
  • Choisir une méthode en fonction de l’action demandée
  • Formater les données pour standardiser la création de la requête
  • Créer 2 requêtes MySql et lancer leur appel vers la base d données.

On commence avec process_action() :


public function process_action()
{
	if( !isset( $_REQUEST[ 'post' ] ) )
	{
		return;
	}
	if( FALSE == ( $current_action = $this->current_action()  ) )
	{
		return;
	}
	if( 
		( !is_array( $_REQUEST[ 'post' ] ) && !filter_var( $_REQUEST[ 'post' ], FILTER_VALIDATE_INT ) ) ||
		( is_array( $_REQUEST[ 'post' ] ) && !wp_verify_nonce( $_REQUEST[ '_wpnonce' ], 'bulk-'.$this->_args[ 'plural' ] ) ) ||
		( !is_array( $_REQUEST[ 'post' ] ) && $current_action == 'delete' && !wp_verify_nonce( $_REQUEST[ $this->deletenoncename ], $this->deletenonceaction ) ) || 
		( filter_var( $_REQUEST[ 'post' ], FILTER_VALIDATE_INT ) && $current_action == 'publish' && !wp_verify_nonce( $_REQUEST[ $this->publishnoncename ], $this->publishnonceaction ) )
	)
	{
		wp_die( 'Vous n’avez pas les droits suffisants pour continuer sur cette page !' );
	}
	if( 'delete' == $current_action )
	{
		$this->action_delete( $_REQUEST[ 'post' ] );
	}
	if( 'publish' == $current_action )
	{
		$this->action_publish( $_REQUEST[ 'post' ] );
	}
}

On crée une méthode pour formater l’ID ou les IDs transmis, et récupérer un tableau (array). On en profite pour vérifier que toutes les entrées sont des entiers positifs :


private function format_ids( $ids )
{
	if( !is_array( $ids ) ) $ids = array( $ids );
	foreach( $ids as $k=>$id )
	{
		if( !filter_var( $id, FILTER_VALIDATE_INT ) || $id < 0 ) return false;
	}
	return $ids;
}

On créé les 2 méthodes pour les 2 requêtes SQL distinctes (on pourrait les fusionner) :


private function action_delete( $ids  )
{
	if( FALSE === ( $ids = $this->format_ids( $ids ) ) )
	{
		return false;
	}
	$ids = implode( ', ', $ids );
	global $wpdb;
	$tablename = $wpdb->prefix.$this->tablename;
	$sql = "
	DELETE FROM {$tablename} 
	WHERE ID in (".$ids.")";
	return $wpdb->query( $sql );
}
private function action_publish( $ids )
{
	if( FALSE === ( $ids = $this->format_ids( $ids ) ) )
	{
		return false;
	}
	
	$ids = implode( ', ', $ids );
	global $wpdb;
	$tablename = $wpdb->prefix.$this->tablename;
	$sql = "
	UPDATE {$tablename} 
	SET post_status = 'publish'  
	WHERE ID in (".$ids.")";
	return $wpdb->query( $sql );
}

Jusqu’ici, nous avons défini nos colonnes, dont la colonne primaire (et ses actions individuelles) et la colonne de la checkbox, défini les colonnes triables, les actions groupées à afficher dans le champ la liste déroulante <select>, et traité les actions utilisateur (qu’elles soient groupées ou non).

Il nous reste à coder deux méthodes, pour récupérer les données et préparer le tableau WordPress. La classe prête, nous créerons notre page d’administration pour l’y instancier et afficher notre tableau.

Récupération des data

La classe WP_List Table ne procure logiquement aucune méthode de récupération des données : le rôle de la classe est d’afficher un tableau, pas de choisir avec quelles data vous l’alimenter. Nous devons donc implémenter notre propre méthode de récupération des données.

Nous récupérons les données paginées en nous aidant des modules de navigation affichés avant et après le bloc <table> (contenu dans des <div> de class « tablenav top » et « tablenav bottom » respectivement). Nous allons nous servir du paramètre d’URL «paged» (contenus dans quasiment tous les liens de pagination sur WordPress) pour construire notre requête SQL. Cette valeur sera récupérée depuis la méthode prepare_items().

À la différence d’Agbonghama Collins, j’inclus l’option SQL_CALC_FOUND_ROWS dans la déclaration SQL (le statement) et je récupère le nombre total d’items en base de données avec un second statement SELECT FOUND_ROWS().

Cela m’évite l’implémentation d’une 2e méthode et d’une 2e requête et me fait gagner en performance. Pour les sceptiques voilà ce qui est déclaré sur la documentation officielle de MySql :

If you are using SELECT SQL_CALC_FOUND_ROWS, MySQL must calculate how many rows are in the full result set. However, this is faster than running the query again without LIMIT, because the result set need not be sent to the client.

Voici à quoi ressemble ma méthode :


private function getDataInfos( $page = 1 )
{
	global $wpdb;
	$page = $page <= 1 ? 1 : (int) $page;

	$posttablename = $wpdb->prefix.$this->posttablename;
	$userstablename = $wpdb->prefix.$this->userstablename;
	$perpage = $this->perpage;

	$sql = 
	"SELECT SQL_CALC_FOUND_ROWS p.ID, post_title title, post_date date, u.user_nicename user 
	FROM {$posttablename} p
	JOIN {$userstablename} u 
	ON p.post_author = u.ID 
	WHERE p.post_status = 'draft' 
	AND p.post_type = 'post'";
	$orderby	= isset( $_REQUEST[ 'orderby' ] ) ? esc_sql( $_REQUEST[ 'orderby' ] ) : 'post_title';
	$order 		= isset( $_REQUEST[ "order" ] )  ? esc_sql( $_REQUEST[ 'order' ] ) :  'ASC';
	$sql .= " ORDER BY ".$orderby." ".$order; 
	$sql .= " LIMIT %d,%d";
		
	$sql = $wpdb->prepare( $sql, ( $page -1 ) * $perpage, $perpage );
	$items = $wpdb->get_results( $sql, ARRAY_A );
	$this->foundrows = $wpdb->get_var( "SELECT FOUND_ROWS()" );

	return array(
		'items'		=> $items,
		'foundrows'	=> $this->foundrows
	);
} 

Nous récupérons et retournons donc en même temps, les données de la page courante et le nombre total d’items (les posts) ciblés par la requête.

On appellera cette fonction depuis la méthode prepare_items(), juste après avoir récupéré notre variable de pagination avec $_REQUEST[ « page » ]. On voit ça maintenant.

Préparation du tableau : en-têtes, data et pagination

Tout est dans le titre !

Nous attribuons à la propriété _column_headers un tableau comportant le tableau des colonnes, celui des champs cachés et celui des colonnes triables.

On cache la colonne du champ «ID» qui n’a pas besoin d’être affichée.

Nous attribuons ensuite le résultat de notre requête SQL à la propriété $this->items.

Nous paramétrons la pagination avec set_pagination_args(). Cette dernière méthode attend un tableau incluant le nombre total d’éléments en base de données, le nombre d’items par page, le nombre de pages, les variables de tri «orderby» et «order» issues de la requête courante ($_REQUEST).


public function prepare_items()
{
	$this->process_action();

	$columns 	= $this->get_columns();
	$sortable 	= $this->get_sortable_columns();
	$hidden 	= ['ID'];

	$this->_column_headers = [
		$columns, $hidden, $sortable
	];
	$paged = ( isset( $_REQUEST[ "paged" ] ) && $_REQUEST[ "paged" ] && $_REQUEST[ "paged" ] > 1) ? (int) $_REQUEST[ 'paged' ] : 1;
	$itemInfos = $this->getDataInfos( $paged );
	
	$this->items = $itemInfos[ 'items' ];
		
	$totalitems = $itemInfos[ 'foundrows' ];
	$totalPages = ceil( $totalitems / $this->perpage );

	$this->set_pagination_args(
		[
			'total_items'	=> $totalitems,
			'per_page'	=> $this->perpage,
			'total_pages'	=> $totalPages,
			'orderby'	=> isset( $_REQUEST[ 'orderby' ] ) ? esc_attr( $_REQUEST[ 'orderby' ] ) : 'title',
			'order'		=> isset( $_REQUEST[ 'order' ] ) ? esc_attr( $_REQUEST[ 'order' ] ) : 'ASC'
		]
	);
}

Il ne nous manque plus qu’une méthode pratique d’instanciation et une page pour afficher le tableau WordPress.

Instanciation de WP_List_Table : la simplicité du singleton

Cette méthode d’instanciation n’a rien à voir avec WP_List_Table, c’est un choix perso…

Le principe est simple. On définit une propriété de classe $_instance à NULL. On crée une méthode d’instanciation retournant cette valeur après l’avoir remplacé par l’instance si elle est positionnée à NULL. La propriété et la méthode sont statiques.


public static function get_instance()
{
	self::$instance = is_null( self::$instance ) ? new self() : self::$instance;
	return self::$instance; 
}

Nous allons nous servir de cette méthode d’instanciation au sein même de la page d’administration sur laquelle nous affichons le tableau.

Une classe différenciée pour le plugin et le tableau de listing

Nous allons faire simple. L’objectif de notre plugin se limitera à l’affichage de notre tableau. Notre classe ne possédera donc pour l’instant que 3 méthodes ayant pour objectif la construction de l’objet, la définition de la page d’administration et le rendu de celle-ci. La seule partie vraiment intéressante du code se trouve au niveau de la méthode list_page(), chargée du rendu de la page d’administration WordPress.

Notez qu’on y récupère l’instance de la classe étendue (via le singleton), et que l’on appelle les méthodes de préparation et d’affichage du tableau depuis cette instance (cette instanciation tardive permet, entre autres, à WP_Screen d’être chargée lorsque le constructeur de WP_List_Table est appelé).

Enfin, l’insertion des balises du formulaire est à notre charge.

Toute dernière chose, j’initialise la classe de plugin en utilisant « plugins_loaded », qui est le hook utilisé lors d’une surcharge de plugin existant.


if( !class_exists( 'JST_Simple_List_Plg' ) )
{
	class JST_Simple_List_Plg
	{
		public $list;
		public function __construct()
		{
			add_action( 'admin_menu', [ $this, 'add_pages' ] );
		}

		public function add_pages()
		{
			$hook = add_menu_page(
				'List Table Stuff',
				'Simple List Table',
				'manage_options',
				basename( __FILE__ ),
				[ $this, 'list_page' ],
				'dashicons-groups'
			);
		}
		public function list_page()
		{
			$this->list = JST_Table::get_instance();
			$this->list->prepare_items();
	?>
			<div class="wrap">
				<h1>Simple List Table</h1>
				<form action="" method="POST">
					
					
					<?php  $this->list->display(); ?>

				</form>
				

			</div>
	<?php
		}
	}
	add_action( 'plugins_loaded', 'load_simple_list_table' );
	function load_simple_list_table()
	{
		new JST_Simple_List_Plg;
	}
}

TADAAAAM !!! Vous voilà en présence d’un tableau WordPress d’administration customisé 😎 !

Tableau WordPress avec WP_List_Table : écosystème et expérience utilisateur

Si vous êtes arrivé jusqu’ici alors que vous ne maîtrisiez pas WP_List_Table, c’est que vous êtes courageux. Mais vous allez vite vous rendre compte qu’avec un peu de pratique sa manipulation devient plus intuitive et que le jeu en vaut la chandelle.

Cet outil permet de présenter, assez simplement, un tableau WordPress correspondant au format proposé sur le reste du système, des fonctionnalités et une souplesse de gestion de données qui ajouteront une véritable valeur ajoutée au plugin. Il nous reste à voir l’implémentation d’AJAX pour notre classe étendue. Le temps de mettre l’article en forme et je vous le livre …

3 commentaires pour Tableau WordPress intégré et WP_List_Table : guide d’utilisation détaillé

  1. Super article, très clair. J’avais bricolé une classe pour faire un WP_List_Table moi-même, mais ça marchait moyen. Celle-ci est très claire.

    Juste une petite coquille : dans la fonction column_cb :

    public function column_cb( $item ){
    return sprintf(
     »,
    $this->_args[ « singular » ],
    $item[ « ID » ]
    );
    }

    Le « valuye » est en fait un « value ». Ca empêche seulement les actions de groupe.

    • Tout à fait exact, c’est rectifié !
      Merci beaucoup 😉

  2. Franchement, je suis épaté. 😮
    Y’a quelques coquilles ( class JST_Table { au lieu de class JST_Table extends WP_List_Table { ) au début, mais ça marche impecc, et le déroulé est très bien fait.
    Ca fait un bout de temps que je me demandais comment faire une liste de post spécifique, sans passer par les CPT, et c’est quelque chose que je n’avais encore pas exploré. Je vais pouvoir adapter tout ça à mes souhaits.
    Big Up à toi ! Bravo !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *