Écran d'adminitration WordPress avec filtres et colonnes de taxonomies personnalisées ajoutés avec le plugin CTFC

Une colonne triable de taxonomie dans la page d’administration des Custom Posts Type WordPress

La gestion des custom post types de WordPress peut être facilitée par l’utilisation de plugins tels que Custom Post UI ou Toolset Types. Or, certains d’entre eux ne prennent pas en charge les CPT existants, ne proposent pas de filtre par taxonomies, ou n’autorisent pas le tri sur une colonne de taxonomie personnalisée. Nous allons voir comment ajouter ces fonctionnalités par nos propres moyens.

Lors d’une récente refonte de site internet, j’ai eu à redéfinir l’organisation des contenus du site web, et fournir aux pages d’administration des Custom Post Types préexistants des filtres et colonnes triables (basés sur leurs taxonomies) devenus nécessaires en raison du gros volume de ressources sur le site.
J’ai donc cherché un moyen d’ajouter ces fonctionnalités à tous les CTP et taxonomies liées déclarés.
Attention : ce système de tri des Custom Posts par termes de taxonomies ne fonctionne actuellement qu’avec des posts mono-catégorisés (une seule catégorie par post). Son utilisation sur des posts multi catégorisés provoque des résultats inattendus, il est donc déconseillé de l’utiliser dans cette situation.

Ajouter un filtre de taxonomie au tableau des Custom Post Types

Si vous tombez directement cet article, vous pouvez lire Ajouter automatiquement un filtre de taxonomie pour chaque Custom Post Type WordPress, qui explique ce processus.
J’y introduis notamment une fonction servant à récupérer les types de contenus non natifs dans WordPress : jst_get_all_non_builtin_custom_post_types() dont nous allons nous resservir ici.

Ajouter une colonne de taxonomie au tableau des Custom Posts

Si vous codez vous-même vos CPT et taxonomies, afficher une colonne de termes dans votre tableau de posts est assez simple. Vous ajoutez l’option « show_admin_column » au tableau d’arguments de la fonction register_taxonomy() :


function my_register_taxonomies()
{
	register_taxonomy(  ‘ma-taxonomie’, ‘mon-post-type’, array(
		‘labels’ 	=> $labels,
		‘public’	=> true,
		'show_admin_column' => true
	)) ;
}

Dans ce ça de figure, vous maîtrisez sans doute le reste de la fonction ;-).

Pour ceux qui ne peuvent toucher à ce code (par exemple dans le cas d’utilisation d’un plug-in tierce partie), il devient nécessaire de créer soi-même une en-tête de colonne ainsi que son contenu, pour la taxinomie liée.

Custom Post Type : ajout d’une colonne de taxonomie personnalisée

Libellé de colonne

Le filtre WordPress « manage_{$post_type}_post_columns » (où $post_type est le Custom Post Type présenté sur l’écran d’administration) permet de définir une colonne supplémentaire pour le tableau des posts listés, et d’y injecter son en-tête (<th>).
Nous allons donc boucler sur les types de contenus personnalisés pour appeler ce hook, et depuis son callback :

  • récupérer le type de posts courant ;
  • retrouver un objet WP_Taxonomy pour chaque taxonomie liée (et accéder à ses libellés) ;
  • ajouter une colonne et son en-tête pour chacune d’entre elles, au tableau des colonnes renvoyé par la fonction.

Voici la fonction de rappel de « manage_{$post_type}_post_columns » :


/**
 * Ajouter une colonne de catégorie
 */
function jst_manage_cpt_posts_columns( $cols )
{
	// Récupération du type de post courant et des taxonomies liées au CPT
	$post_type = get_current_screen()->post_type; 
	$taxonomies = get_object_taxonomies( $post_type );
	foreach( $taxonomies as $tax_id )
	{
		// Récupération de l’objet WP_Taxonomy
		$tax = get_taxonomy( $tax_id );
		// Ajout d’une colonne par définition de l’ attribut id de son en-tête
		$cols[ 'taxonomy-' . $tax_id ] = $tax->label;
	}
	return $cols;
}
Page d'administration d'un Custom Post Type WordPress avec en-tête d'une colonne de taxonomie

Contenu de la colonne

La bonne nouvelle est que si vous optez pour le schéma de nommage vu précédemment (« taxonomy- ».$tax_id) pour l’ID de l’en-tête de cette colonne, WordPress va automatiquement remplir pour vous les cellules liées à chaque ligne de post.
Un coup d’œil à méthode column_default() de la class WP_Post_List_Table nous permet de comprendre comment se fait l’extraction de la taxonomie depuis $column_name :


public function column_default( $post, $column_name ) {
	if ( 'categories' === $column_name ) {
		$taxonomy = 'category';
	} elseif ( 'tags' === $column_name ) {
		$taxonomy = 'post_tag';
	} elseif ( 0 === strpos( $column_name, 'taxonomy-' ) ) {
		$taxonomy = substr( $column_name, 9 );
	} else {
		$taxonomy = false;
	}
	if ( $taxonomy ) { 
		// Recupération du terme et lien associé

Mais si vous désirez attribuer un ID personnalisé à l’en-tête de votre colonne, il vous faudra peupler vos cellules à l’aide de l’action « manage_{$post_type_id}_posts_custom_column ».
La fonction de rappel de ce hook étant appelée pour chaque colonne du tableau, elle reçoit l’ID de la colonne courante et l’ID du post ($post->ID) pour la ligne courante.

Nous allons donc :

  • récupérer le type de post et ses taxonomies ;
  • boucler sur les taxonomies liées si elles existent ;
  • cibler la colonne définie pour la taxonomie courante ;
  • Récupérer les termes de la taxonomie (catégories) assignés au post ;
  • afficher dans la cellule le terme principal utilisé pour l’article (sa « catégorie ») et un lien servant de filtre pour le tableau.

N.B. Le lien permettant de filtrer le tableau en fonction du terme affiché est construit de façon similaire à la requête GET utilisée pour les filtres :

  • la page cible est « edit.php » ;
  • le CPT est transmis en valeur de la variable « post_type » ;
  • le nom de la variable de filtre est l’identifiant de la taxonomie ;
  • sa valeur est le slug du terme lié au post.

Pour rester le plus simple possible, je conserve ici le schéma de nommage « taxonomy- » . $tax_id, mais testez le code avec votre propre schéma d’identifiant pour voir le résultat de chaque hook sur votre tableau :


/**
 * Remplit la colonne de terme (taxonomie personnalisée) pour chaque post (ligne)
 */
function jst_manage_cpt_posts_custom_column( $col, $post_id )
{
	// Post Type courant & taxonomies du Post Type courant
	$post_type = get_current_screen()->post_type; 
	$taxonomies = get_object_taxonomies( $post_type );
	// Itération
	foreach( $taxonomies as $tax_id )
	{
		// En fonction de l’identifiant de la colonne
		switch( $col )
		{
			case 'taxonomy-'.$tax_id :
				// Si au moins un terme de cette taxonomie est attaché au post 
				if( FALSE !== ( $the_terms = get_the_terms( $post_id, $tax_id ) ) )
				{
					// Récupération du terme principal
					$terme = $the_terms[ 0 ];
					// URL d’administration du CPT… mais filtrée par le terme
					$term_link = admin_url( 'edit.php?post_type='.$post_type.'&'.$tax_id.'='.$term->slug );
					printf( 
						'<a href="%1$s">%2$s</a>', 
						$term_link, 
						$terme->name
					);
				}
				break;
			default:
				break;
		}
	}
}
Cellules de colonnes de taxonomie, page d'administration de Custom Post Type WordPress

Une colonne de taxonomie, oui, mais triable

Nous souhaitons ici rendre triables toutes les colonnes des taxonomies attachées à chaque type de contenu non natif.
WordPress met à notre disposition un filtre permettant d’agir sur le tableau des colonnes triables : «  manage_edit-{$post_type_id}_sortable_columns ». Sa fonction de rappel prend en argument le tableau des colonnes à trier, nous allons donc lui fournir les identifiants des en-têtes déclarées plus tôt.


/**
 * Rend triable chaque colonne de taxonomie prise en charge
 */
function jst_manage_edit_cpt_posts_sortable_custom_column( $sortable )
{
	$post_type = get_current_screen()->post_type;
	$taxonomies = get_object_taxonomies( $post_type );
	// Pour chaque taxonomie
	foreach( $taxonomies as $tax_id )
	{
		// Ajout de l’ID de la colonne au tableau des colonnes triables
		$sortable[ 'taxonomy-'.$tax_id ] = $tax_id ;
	}
	return $sortable;
}
Colonne de taxonomie triable, page d'administration de Custom Post Type WordPress

Nous allons maintenant réutiliser notre méthode jst_get_all_non_builtin_custom_post_types() pour réaliser une itération sur nos types de contenu.
Mais puisque certaines taxonomies personnalisées sont créées au sein de plugins WordPress, nous allons retarder un peu le processus global, et attendre qu’ils soient tous chargés pour initialiser notre boucle.
L’action « plugins_Loaded » semble un peu tardive, nous passerons donc par « init ».

Votre devis en 48 H chrono !

Demandez à être rappelé !

Nous préciserons ensemble votre projet
de vive voix 

N.B. Ceux qui utilisent l’option « show_admin_column » (dans la déclaration de leurs propres taxonomies) pourront supprimer ou commenter les 2 premiers hooks.
N.B. Ceux qui utilisent un schéma de nommage « taxonomy- » .$tax_id lors de la création des en-têtes pourront supprimer ou commenter le 2e hook :


/**
 * Pour chaque Custom Post Type
 * appel des hooks nécessaires pour créer les colonnes triables des taxonomies personnalisées
 */
function jst_loops_through_cpts()
{
	
	$all_post_types = jst_get_all_non_builtin_custom_post_types();
	
	foreach( $all_post_types as $post_type_id )
	{
		add_filter( 'manage_'.$post_type_id.'_posts_columns', 'themework_manage_cpt_posts_columns' );
		add_action( 'manage_'.$post_type_id. '_posts_custom_column', 'themework_manage_cpt_posts_custom_column', 10, 2 );
		add_filter( 'manage_edit-'.$post_type_id.'_sortable_columns', 'themework_manage_edit_cpt_posts_sortable_custom_column' );
	}
}

/**
 * On attend que tous les CPT et taxonomies soient chargés
 */
add_action( 'init', ‘jst_loops_through_cpts' );

Et voilà ! Nous avons bien une colonne triable pour chaque taxonomie, mais là… et bien ça ne marche pas !
C’est tout à fait normal. Après avoir cliqué sur l’en-tête de la colonne de taxonomie, on peut observer dans notre requête GET, que la variable « orderby » est positionnée sur l’identifiant de notre taxonomie. C’est bien nécessaire, mais pas suffisant pour accéder au résultat attendu.
Nous allons devoir modifier la requête SQL pour prendre en compte notre tri.

Construction d’une nouvelle requête SQL

Afin de comprendre à quoi on a à faire, jetons un œil à une requête SQL permettant d’afficher des Custom Post Types relatifs à une certaine taxonomie, triés par termes (nos catégories en quelque sorte) :


SELECT terms.name, p.* FROM wp_posts p 
LEFT JOIN wp_term_relationships tr ON p.ID = tr.object_id 
LEFT JOIN wp_term_taxonomy termtax USING( term_taxonomy_id ) 
LEFT JOIN wp_terms terms USING (term_id) 
WHERE p.post_type = 'jst_basic' 
AND termtax.taxonomy = 'jst_cpt_type' 
ORDER BY terms.name ASC

En base de données, chaque post ou custom post type de WordPress est associé à une taxonomie de termes (table « wp_term_taxonomy ») à travers une relation formalisée par une table intermédiaire « wp_term_relationships ».
Afin de mieux comprendre l’organisation des taxonomies de WordPress, on peut observer que :

  • la table « wp_term_taxonomie » stocke directement les identifiants des termes dans la colonne « term_id » ;
  • elle permet aussi de regrouper ces termes dans des ensembles nommés, au sein de la colonne « taxonomy » ;
  • la description pour chaque terme est contenue dans sa colonne « description » ;
  • les noms et slug des termes sont, quant à eux, stockés dans la table « wp_term ».

Le trie de notre tableau se fera donc sur le nom des termes associés à la taxonomie via « wp_terms.name », et il nous faut faire une jointure (la jonction) entre les tables « wp_posts », « wp_term_relationships », « wp_term_taxonomy » et « terms ».

Nous allons reconstruire les clauses de la requête SQL gérée par WP_Query, en nous aidant du filtre WordPress « posts_clauses » (qui nous permet d’intervenir sur les éléments de requêtes SQL avant leur soumission en base de données).

N.B. Le callback de « posts_clauses » propose 2 paramètres, dont une instance de $wp_query (et donc de $wp_query->query_vars). Nous allons principalement utiliser $_GET pour accéder à nos variables (le type de post courant pourrait aussi être récupéré via get_current_screen()->post_type).

Dans l’ordre :

  • restriction de l’action au seul tri (pas de traitement en cas de soumission du formulaire de filtre) sur l’écran « edit.php » ;
  • récupération des paramètres « order » et « orderby » ;
  • validation du type Custom Post Type ;
  • récupération des taxonomies du CPT ;
  • récupération de l’identifiant de taxonomie utilisé pour filtrer notre CPT (« orderby ») ;
  • validation de la correspondance « orderby » avec une taxonomie du CTP ;
  • récupération des noms préfixés de tables SQL (avec $wpdb->prefix) ;
  • reconstruction et revoie des clauses SQL.

N.B. Si les conditions ne sont pas satisfaites, on retourne les clauses en l’état.


/**
 * Reconstruction des clauses SQL de WP_Query
 */
function jst_posts_clauses( $clauses, $wp_query )
{
	global $pagenow;
	/**
 	* Si le filtre est utilisé nous retournons les clauses telles quelles
 	* Vérification de la présence des paramètres de requêtes GET
 	*/
	if( isset( $_GET[ 'filter_action' ] ) || 
		'edit.php' != $pagenow || 
		! isset( $_GET[ 'post_type' ] ) || 
		! isset( $_GET[ 'orderby' ] ) || 
		! isset( $_GET[ 'order' ] ) )
	{
		return $clauses;
	}
	// Récupération des CPT et du type courant, Validation du CPT	
	$all_cpts = jst_get_all_non_builtin_custom_post_types();
	$requested_post_type = $_GET[ 'post_type' ];
	if( ! in_array( $requested_post_type, $all_cpts ) )
	{
		return $clauses;
	}

	/**
	 * Récupération des paramètres GET « orderby » et « order »
	 * « orderby » doit correspondre à une taxonomie triée
	 */
	$orderby_taxonomy = esc_sql( $_GET[ 'orderby' ] );
	$order =  ( isset( $_GET[ 'order' ] ) && 'desc' == $_GET[ 'order' ] ) ? 'DESC' : 'ASC';
	// Le CPT courant doit être lié à la taxonomie triée
	$cpt_taxonomies = get_object_taxonomies( $requested_post_type );
	if( !in_array( $orderby_taxonomy, $cpt_taxonomies ) )
	{
		return $clauses;
	}
	// Tables SQL
	global $wpdb;
	$table_term_relationships 	= $wpdb->prefix.'term_relationships';
	$table_term_taxonomy 		= $wpdb->prefix.'term_taxonomy';
	$table_terms 			= $wpdb->prefix.'terms';
	$table_posts			= $wpdb->prefix.'posts';
	// $clause[ ‘join’ ] doit inclure la cause « JOIN »
	$clauses[ 'join' ]		= " LEFT JOIN ".$table_term_relationships. " tr ON (".$table_posts.".ID = tr.object_id)";
	$clauses[ 'join' ]		.= " LEFT JOIN ".$table_term_taxonomy." termtax USING ( term_taxonomy_id )";
	$clauses[ 'join' ]		.= " LEFT JOIN ".$table_terms." terms USING (term_id)";
	$clauses[ 'distinct' ]		.= "DISTINCT";
	// Ajout d’une condition à la clause $clause[ ‘where’ ] 
	$clauses[ 'where' ] 		.= " AND termtax.taxonomy = '".$orderby_taxonomy."'";
	// Définition de la clause $clause[ ‘orderby’ ] sur les noms des termes de la taxonomie
	$clauses[ 'orderby' ] 		= "terms.name ".$order;
	return $clauses;
} 
add_filter( 'posts_clauses', 'jst_posts_clauses', 10, 2 );

N.B. La clé « join » du tableau des clauses SQL requiert la clause formelle « JOIN » dans la chaîne, alors que les clés « where » et « orderby » n’attendent que la condition SQL.
Enfin, la clé « orderby » inclut le paramètre ORDER (DESC ou ASC).

Page d'administration de Custom Post Type WordPress, triée par termes de taxonomie

Voilà nos tableaux de Custom Post Types, enfin doté de colonnes de taxonomies triables ;-).

Limitation de la prise en charge des Custom Post Types et taxonomies personnalisées

Une petit problème persiste toutefois après la mise en œuvre de ce système. Cette solution, sans doute un peu trop générique, procure de nouveaux filtres et colonnes pour tous les Custom Post Types et leurs taxonomies, y compris ceux qui proposent déjà cette fonctionnalité.
Pour exemple, les plugin comme « Toolset Types » ou « Custom Post Type UI » proposent une colonne non triable pour les taxonomies liées au types de posts créés (mais pas de filtre).
« Custom Post Type UI » utilisant notre convention de nommage profitera pleinement de notre système (la colonne deviendra triable).
D’autres plugins WordPress comme Event Organiser propose déjà les deux types de fonctionnalités, mais la colonne des catégories n’est souvent pas non plus triable.

La profusion et la diversité des thèmes et plugins WordPress développés peuvent nous amener à implémenter un système souple de limitation ou à rechercher une solution « à la carte », permettant de créer des filtres ou des colonnes triables en fonction de nos besoins.

Je vous propose donc de tester le petit plugin fait maison « Filtres et colonnes de taxonomies personnalisées » qui offre cette fonctionnalité.

Ce plugin gérera aussi les posts multi-catégorisés (même si, du coup, le résultat des tris sera forcément un peu moins pertinent) et vous permettra de décider d’activer ou pas ces fonctionnalités pour chaque CPT et taxonomie personnalisée, et de trier vos éléments lorsque le volume de vos document l’impose.

N’hésitez pas à me communiquer vos impressions et autres suggestions 😉

Quoi qu’il en soit vous voilà maintenant prêt à gérer finement vos type de contenus personnalisés sur WordPress !