Turns out, I had to use a 'posts_orderby' filter. I turned to Sally CJ's answers to my question and another question. I thought I wouldn't need it, but I was wrong.
The reason posts do not get ordered properly is that in the MySQL query WP produces, the postmeta table get joined several times, resulting in multiple meta_key
columns. So, in the first of the meta_key columns, where the meta key isn't 'tdlrm_mp_'.$taxonomy_term
, there still is some other meta key, so the posts just keep being ordered by the meta values corresponding to this column, like so:
/*
| wp_postmeta.meta_key | wp_postmeta.meta_value | mt1.meta_key | mt1.meta_value |
----------------------------------------------------------------------------------
| tdlrm_mp_1494 | 1 | tdlrm_mp_1494 | 1 |
----------------------------------------------------------------------------------
| tdlrm_mp_1494 | 2 | tdlrm_mp_1494 | 2 |
----------------------------------------------------------------------------------
| some_other_key | X | NULL | NULL |
----------------------------------------------------------------------------------
| some_other_key | Y | NULL | NULL |
----------------------------------------------------------------------------------
*/
To deal with this, we need do add case statements to our ORDER BY, so that the meta_value column would be used only to order posts that actually have tdlrm_mp_1494
meta key in it.
We can achieve this by setting up an orderby filter.
This is how:
Here are our meta query arguments. Note the orderby
argument set to false (we do not need it).
$taxonomy = 1494; // for demonstration purposes
$args['meta_query'] = array(
'relation' => 'OR',
'mp_exists' => array(
'key' => 'tdlrm_mp_'.$taxonomy,
'type' => 'NUMERIC',
),
'mp_not_exists' => array(
'key' => 'tdlrm_mp_'.$taxonomy,
'compare' => 'NOT EXISTS'
)
);
$args['orderby'] = false;
First, we add a custom variable to our query arguments, to let WP know where to use the filter:
$args['tdlrm_commands'] = array(
'filter' => 'orderby_filter_1',
'key' => 'tdlrm_mp_'.$taxonomy
);
Second, we add the filter (to functions.php or a plugin file, whatever):
add_filter( 'posts_orderby', function ( $orderby, $query ) {
//won't fire for other queries
if(
empty($query->query['tdlrm_commands']['do'])
|| $query->query['tdlrm_commands']['do'] !== 'orderby_filter_1'
)
{
return $orderby;
}
global $wpdb;
$meta_clauses = $query->meta_query->get_clauses();
//get the key we passed in our custom argument
$key = $query->query['tdlrm_commands']['key'];
//get the table alias (remember there are multiple postmeta tables joined together,
//we need only the one that satisfies our meta query clause, `mp_exists`)
$has_tdlrm_mp = $meta_clauses['mp_exists']['alias'];
/*
in the postmeta table with alias {$has_tdlrm_mp},
when meta_key equals to {$key},
use the first clause,
otherwise, use the second.
first clause:
in the postmeta table with alias {$has_tdlrm_mp},
when meta_key equals to {$key},
order by its meta value, in the ascending order
(+0 casts a string to number, otherwise 21 will go after 2 and before 3.)
second clause:
if it's not, order by title, in the ascending order
*/
$orderby = " CASE {$has_tdlrm_mp}.meta_key
WHEN '{$key}' THEN 1
ELSE 2
END ASC,
CASE {$has_tdlrm_mp}.meta_key
WHEN '{$key}' THEN {$has_tdlrm_mp}.meta_value+0
END ASC,
{$wpdb->posts}.post_title ASC";
return $orderby;
}, 10, 2 );