For the past several days I’ve been hammering my head against a conundrum: how to get WordPress to sort a set of posts in ascending order, but with empty values at the end of the list instead of the beginning.
This seems like it should be a simple option in the query. But MySQL doesn’t offer a straightforward way to do this. There are some fairly simple MySQL tricks that will accomplish it, but there’s no way to apply those tricks within the context of WP_Query
because they require manipulating either the SELECT
or ORDER BY
portions of the SQL query in ways WP_Query
doesn’t allow. (I mean, you can write custom SQL for WP_Query, but if you’re trying to alter the output of the main query, good luck.)
I tried everything I could possibly think of yesterday with the pre_get_posts
hook, but it all went nowhere, other than discovering a very weird quirk of MySQL that I don’t fully understand and won’t bother explaining here.
Sleep on it
I woke up this morning with an idea! I resigned myself to the fact that this ordering can’t happen before the query runs, but I should be able to write a pretty simple function to do it after the query has run.
Bear one key thing in mind: This is not going to work properly with paginated results. I mean, it’ll sort of work. The empty values will get sorted to the end of the list, but they’ll stay on the same “page” they were on before the query was run. In other words, they’ll be sorted to the bottom of page one, not of the last page. Anyway… consider this most useful in cases where you’re setting posts_per_page
to -1
or some arbitrarily large number (e.g. 999
).
The function
This simple (and highly compact) function accepts a field name (and a boolean for whether or not it’s a custom field [meta data]), then takes the array of posts in the main query ($wp_query
), splits them into two separate arrays — one with the non-empty values for your selected field, one with the empty values — and then merges those arrays back together, with all of the non-empty values first. (Other than shifting empties to the back, it retains the same post order from the original query.)
function sort_empty_last($field, $is_meta=false) {
global $wp_query;
if (!$wp_query->is_main_query()) { return; }
$not_empty = $empty = array();
foreach ((array)$wp_query->posts as $post) {
$field_value = !empty($is_meta) ? get_post_meta($post->ID, $field) : $post->{$field};
if (empty(implode((array)$field_value))) { $empty[] = $post; }
else { $not_empty[] = $post; }
}
$wp_query->posts = array_merge($not_empty,$empty);
}
Calling the function
As I said, this function is designed to work directly on the main query. You just need to call the function right before if (have_posts())
in any archive template where you want it to apply. Because of the way it works — especially the posts_per_page
consideration — I thought calling it directly in the template was the most clear-cut way to work with it. Here’s an example of the first few lines of a really basic archive template that uses it, looking for a custom field (meta data) called deadline
:
<?php
get_header();
sort_empty_last('deadline', true);
if (have_posts()) {