OrderBy multiple custom meta fields

EDIT (Please see the original answer for full explanation of the code)

The code in my original answer works as expected, but triggers a known bug with usort (Check the bug report here #50688)

WARNING Error: [2] usort(): Array was modified by the user comparison function

This bug is triggered only when the fields parameter is set in the arguments in get_posts, so the only workable workaround I could find was to remove the fields parameter and go with full posts being retrieved.

Here is the bug free working version

<?php
$args = array (
  'post_type'              => 'member_data',
  'post_status'            => 'active',
  'posts_per_page'         => -1,
  'meta_query'             => array(
    array(
      'key'       => 'first_name',
    ),
    array(
      'key'       => 'last_name',
    ),
    array(
      'key'       => 'publicly_listed',
      'value'     => 'true',
    ),
  ),
);

$posts = get_posts( $args );

usort( $posts, function( $a, $b ){

    // sort first by last name
    $compare = strnatcmp(get_post_meta($a->ID, 'last_name', true), get_post_meta($b->ID, 'last_name', true));

    // if last names are identical, sort by first name
    if(!$compare) {
        return strnatcmp(get_post_meta($a->ID, 'first_name', true), get_post_meta($b->ID, 'first_name', true));
    }else{
        return $compare;
    }

});

if( $posts ) {
   echo "Total Members Found: " . count($posts) . "<br>";

   foreach ( $posts as $post ) {
      $q = get_post_meta($post->ID);
      echo "<b>" . $q['first_name'][0] . " " . $q['last_name'][0] . "</b><br>";
      echo $q['city'][0] . ", " . $q['state'][0] . " " . $q['zip'][0] . "<br>";
      echo "<a href="https://wordpress.stackexchange.com/questions/166522/mailto:" . $q["email'][0] . "'>" . $q['email'][0] . "</a><br>";
      echo "<br>"; 
   }
   unset( $post );       
}else{
  echo "<b>Sorry, we found no active members. Please be sure to check back soon.</b>";
}
}

ORIGINAL ANSWER

Unfortunately there is no way to do this type of sorting natively, so you will need to take a different approach here.

  • Use WP_Query, or even better get_posts retrieve your posts as normal. There is no need to apply any sorting at this stage. In my personal opinion, this is useless as you can sort only by one meta_value and not two. I would rather just use php sorting to order the posts as needed. (If you are going to use pagination, rather use WP_Query).

  • You are going to use usort to sort your posts, first by last_name, then if two or more persons share the same last_name, we will sort according to first_name.

Here is the concept:

Before I start, I want to point out a few things

  • Your meta_query is a mess unfortunately. Everything is within an array within an array within an array, where as it should be 3 separate arrays within one array

  • I would rather use get_posts here and also only retrieve the post ID’s as you aren’t going to make use of any postdata or any of the other data returned by WP_Query

  • This is quite a heavy operation as the db is visited two times for each post. You will have to look to make use of transients here.

Now for the solution

As said use get_posts to construct your query with. We are going to retrieve the posts which matches all three meta_key‘s

$args = array (
  'post_type'              => 'member_data',
  'fields'                 => 'ids',
  'post_status'            => 'active',
  'posts_per_page'         => -1,
  'meta_query'             => array(
    array(
      'key'       => 'first_name',
    ),
    array(
      'key'       => 'last_name',
    ),
    array(
      'key'       => 'publicly_listed',
      'value'     => 'true',
    ),
  ),
);

$posts = get_posts( $args );

We are now going to sort $posts by last_name then first_name using usort

usort( $posts, function( $a, $b ){

    // sort first by last name
    $compare = strnatcmp(get_post_meta($a, 'last_name', true), get_post_meta($b, 'last_name', true));

    // if last names are identical, sort by first name
    if(!$compare) {
        return strnatcmp(get_post_meta($a, 'first_name', true), get_post_meta($b, 'first_name', true));
    }else{
        return $compare;
    }

});

$posts should now be sorted alphabetically by last_name, and if the last_name is the same for two or more persons, they will be sorted in alphabetical order by first_name.

Your loop will now follow and look like this

if( $posts ) {
   echo "Total Members Found: " . count($posts) . "<br>";

   foreach ( $posts as $post ) {
      $q = get_post_meta($post);
      echo "<b>" . $q['first_name'][0] . " " . $q['last_name'][0] . "</b><br>";
      echo $q['city'][0] . ", " . $q['state'][0] . " " . $q['zip'][0] . "<br>";
      echo "<a href="https://wordpress.stackexchange.com/questions/166522/mailto:" . $q["email'][0] . "'>" . $q['email'][0] . "</a><br>";
      echo "<br>"; 
   }
   unset( $post );       
}else{
  echo "<b>Sorry, we found no active members. Please be sure to check back soon.</b>";
}
}