Il m’est parfois arrivé de devoir lister des articles en les ordonnant selon la valeur d’une meta (ce que WordPress permet de faire facilement), mais sans pour autant exclure les articles n’ayant pas cette meta (c’est là que ça se complique). Voici comment j’ai résolu ce problème.
Tout d’abord, comment ordonner des articles selon une meta ?
Pour les exemples qui suivent, je vais utiliser un CPT « hotel » et une post meta « _stars » (nombre).
1234567
$hotels = new WP_Query( array(
'post_type' => 'hotel'
'post_status' => 'publish',
'meta_key' => '_stars',
'orderby' => 'meta_value_num',
'order' => 'DESC',
) );
Pour orderby on utilise donc meta_value
ou meta_value_num
.
En faisant ceci, on va créer une meta query, et donc WordPress ne va retourner que les résultats ayant cette meta. Le problème c’est que ce comportement n’est pas toujours désiré. Après tout, on n’a pas (vraiment) demandé une meta query, on veut juste changer l’ordre .
Alors comment faire ? Pour cela il va falloir modifier la requête SQL.
Par exemple, voilà à quoi ressemble une requête classique :
0102030405060708091011
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts
INNER JOIN wp_postmeta
ON ( wp_posts.ID = wp_postmeta.post_id )
WHERE 1=1
AND wp_posts.post_type = 'hotel'
AND ( wp_posts.post_status = 'publish' )
AND ( wp_postmeta.meta_key = '_stars' )
GROUP BY wp_posts.ID
ORDER BY CAST(wp_postmeta.meta_value AS SIGNED) DESC
LIMIT 0, 10
Et voilà ce qu’il nous faudrait :
01020304050607080910
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts
LEFT JOIN wp_postmeta
ON ( wp_posts.ID = wp_postmeta.post_id AND wp_postmeta.meta_key = '_stars' )
WHERE 1=1
AND wp_posts.post_type = 'hotel'
AND ( wp_posts.post_status = 'publish' )
GROUP BY wp_posts.ID
ORDER BY CAST(wp_postmeta.meta_value AS SIGNED) DESC
LIMIT 0, 10
D’abord il faut supprimer la partie AND ( wp_postmeta.meta_key = '_stars' )
dans la clause WHERE
pour ensuite l’ajouter dans la jointure JOIN
. Dernière étape, INNER JOIN
devient LEFT JOIN
.
Histoire que le code soit facilement réutilisable, on va « créer » un paramètre personnalisé : 'orderby_meta_include_null'
. En précisant ce paramètre on indique clairement que l’on souhaite avoir également les éléments sans meta.
12345678
$hotels = new WP_Query( array(
'post_type' => 'hotel'
'post_status' => 'publish',
'meta_key' => '_stars',
'orderby' => 'meta_value_num',
'order' => 'ASC',
'orderby_meta_include_null' => 1, // Ouais bon, ça déborde un peu.
) );
Voilà ce que ça donne. Ceci trouvera sa place dans un MU Plugin :
01020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// !When ordering by meta value, include posts without meta.
add_filter( 'posts_clauses', 'sf_orderby_meta_include_null', 1, 2 );
function sf_orderby_meta_include_null( $pieces, $query ) {
global $wpdb;
if ( ! $query->get( 'orderby_meta_include_null' ) && ! $query->get( 'orderby_meta_include_null_at_the_end' ) ) {
return $pieces;
}
$orderby = $query->get( 'orderby' );
if ( is_array( $orderby ) ) {
if ( ! isset( $orderby['meta_value'] ) && ! isset( $orderby['meta_value_num'] ) ) {
return $pieces;
}
}
elseif ( is_string( $orderby ) ) {
if ( strpos( $orderby, 'meta_value' ) === false ) {
return $pieces;
}
}
else {
return $pieces;
}
if ( ! $meta_key = $query->get( 'meta_key' ) ) {
return $pieces;
}
$meta_table = $query->meta_query->meta_table;
$meta_key = esc_attr( $meta_key );
if ( ! preg_match( '@(' . $meta_table . '|mt\d+)\.meta_key\s*=\s*\'' . $meta_key . '\'@', $pieces['where'], $meta_prefix ) ) {
return $pieces;
}
$meta_prefix = $meta_prefix[1];
// Remove the meta_key statement.
$pieces['where'] = preg_replace(
'@AND\s*\(\s*' . $meta_prefix . '\.meta_key\s*=\s*\'' . $meta_key . '\'\s*\)@',
'',
$pieces['where']
);
// Add the meta_key statement and change the INNER JOIN to LEFT JOIN.
$pieces['join'] = str_replace(
'INNER JOIN ' . $meta_prefix . ' ON ( ' . $wpdb->posts . '.ID = ' . $meta_prefix . '.post_id )',
'LEFT JOIN ' . $meta_prefix . ' ON ( ' . $wpdb->posts . '.ID = ' . $meta_prefix . '.post_id AND ' . $meta_prefix . '.meta_key = \'' . $meta_key . '\' )',
$pieces['join']
);
if ( $query->get( 'orderby_meta_include_null_at_the_end' ) ) {
$meta_type = $query->get( 'meta_type' );
$cast_prefix = $meta_type ? 'CAST(' : '';
$cast_suffix = $meta_type ? ' AS ' . WP_Meta_Query::get_cast_for_type( $meta_type ) . ')' : '';
$pieces['orderby'] = str_replace(
array(
$cast_prefix . $meta_prefix . '.meta_value' . $cast_suffix . ' ASC',
$cast_prefix . $meta_prefix . '.meta_value' . $cast_suffix . ' DESC',
),
array(
'( CASE WHEN ' . $meta_prefix . '.meta_value IS NULL THEN 1 ELSE 0 END ) ASC, ' . $cast_prefix . $meta_prefix . '.meta_value' . $cast_suffix . ' ASC',
'( CASE WHEN ' . $meta_prefix . '.meta_value IS NULL THEN 1 ELSE 0 END ) DESC, ' . $cast_prefix . $meta_prefix . '.meta_value' . $cast_suffix . ' DESC',
),
$pieces['orderby']
);
}
return apply_filters( 'sf_orderby_meta_include_null_clauses', $pieces, $query, $meta_key, $meta_prefix );
}
Si vous avez l’œil (et lu le code ^^’), vous avez peut-être remarqué ceci : $query->get( 'orderby_meta_include_null_at_the_end' )
. C’est un autre paramètre personnel pour indiquer que l’on souhaite avoir les éléments sans meta « à la fin ».
En effet, en ordre DESC
on aura normalement les résultats classés comme ceci (valeur de la meta) :
5
4
4
4
2
1
null
null
null
Ce qui donne avec ASC
:
null
null
null
1
2
4
4
4
5
Aïe, les résultats sans meta sont en premier, ce n’est peut-être pas ce que l’on voudrait. Avec ce nouveau paramètre on peut avoir ceci :
1
2
4
4
4
5
null
null
null
Attention car si on repasse en DESC
en gardant ce paramètre, les null
se retrouveront au début !
Il suffit donc de remplacer notre premier paramètre par ce nouveau :
12345678
$hotels = new WP_Query( array(
'post_type' => 'hotel'
'post_status' => 'publish',
'meta_key' => '_stars',
'orderby' => 'meta_value_num',
'order' => 'ASC',
'orderby_meta_include_null_at_the_end' => 1, // Je sais faire encore plus long quand je suis en forme :)
) );
Bien sûr ça marche sur pre_get_posts
:
010203040506070809101112
add_action( 'pre_get_posts', 'yolo_order_by_meta_value' );
function yolo_order_by_meta_value( $query ) {
if ( $query->is_main_query() && $query->is_post_type_archive( 'hotel' ) ) {
$query->set( 'meta_key', '_stars' );
$query->set( 'orderby', 'meta_value_num title' );
$query->set( 'order', 'DESC' );
$query->set( 'orderby_meta_include_null', 1 );
}
}
Il faudra tout de même faire gaffe à la compatibilité avec les autres extensions, certaines pourraient venir perturber le fonctionnement en venant elles aussi modifier les requêtes. Il en va de même pour des modifications dans le core lors de mises à jour, qui pourraient faire échouer les preg_*
et str_replace()
.
Nota : le cas où on indique plusieurs ordres dans orderby
(comme ci-dessus), ainsi que la nouvelle notation avec tableau (WP 4.0) sont prises en compte. L’exemple ci-dessus (meta_value_num title
+ orderby_meta_include_null_at_the_end
) ne marchera qu’à partir de WP 4.0.
See ya!