How to filter users on admin users page by custom meta field?

UPDATE 2018-06-28

While the code below mostly works fine, here is a rewrite of the code for WP >=4.6.0 (using PHP 7):

function add_course_section_filter( $which ) {

    // create sprintf templates for <select> and <option>s
    $st="<select name="course_section_%s" style="float:none;"><option value="">%s</option>%s</select>";
    $ot="<option value="%s" %s>Section %s</option>";

    // determine which filter button was clicked, if any and set section
    $button = key( array_filter( $_GET, function($v) { return __( 'Filter' ) === $v; } ) );
    $section = $_GET[ 'course_section_' . $button ] ?? -1;

    // generate <option> and <select> code
    $options = implode( '', array_map( function($i) use ( $ot, $section ) {
        return sprintf( $ot, $i, selected( $i, $section, false ), $i );
    }, range( 1, 3 ) ));
    $select = sprintf( $st, $which, __( 'Course Section...' ), $options );

    // output <select> and submit button
    echo $select;
    submit_button(__( 'Filter' ), null, $which, false);
}
add_action('restrict_manage_users', 'add_course_section_filter');

function filter_users_by_course_section($query)
{
    global $pagenow;
    if (is_admin() && 'users.php' == $pagenow) {
        $button = key( array_filter( $_GET, function($v) { return __( 'Filter' ) === $v; } ) );
        if ($section = $_GET[ 'course_section_' . $button ]) {
            $meta_query = [['key' => 'courses','value' => $section, 'compare' => 'LIKE']];
            $query->set('meta_key', 'courses');
            $query->set('meta_query', $meta_query);
        }
    }
}
add_filter('pre_get_users', 'filter_users_by_course_section');

I incorporated several ideas from @birgire and @cale_b who also offers solutions below that are worth reading. Specifically, I:

  1. Used the $which variable that was added in v4.6.0
  2. Used best practice for i18n by using translatable strings, e.g. __( 'Filter' )
  3. Exchanged loops for the (more fashionable?) array_map(), array_filter(), and range()
  4. Used sprintf() for generating the markup templates
  5. Used the square bracket array notation instead of array()

Lastly, I discovered a bug in my earlier solutions. Those solutions always favor the TOP <select> over the BOTTOM <select>. So if you selected a filter option from the top dropdown, and then subsequently select one from the bottom dropdown, the filter will still only use whatever value was up top (if it’s not blank). This new version corrects that bug.

UPDATE 2018-02-14

This issue has been patched since WP 4.6.0 and the changes are documented in the official docs. The solution below still works, though.

What Caused the Problem (WP <4.6.0)

The problem was that the restrict_manage_users action gets called twice: once ABOVE the Users table, and once BELOW it. This means that TWO select dropdowns get created with the same name. When the Filter button is clicked, whatever value is in the second select element (i.e. the one BELOW the table) overrides the value in the first one, i.e. the one ABOVE the table.

In case you want to dive into the WP source, the restrict_manage_users action is triggered from within WP_Users_List_Table::extra_tablenav($which), which is the function that creates the native dropdown to change a user’s role. That function has the help of the $which variable that tells it whether it is creating the select above or below the form, and allows it to give the two dropdowns different name attributes. Unfortunately, the $which variable doesn’t get passed to the restrict_manage_users action, so we have to come up with another way to differentiate our own custom elements.

One way to do this, as @Linnea suggests, would be to add some JavaScript to catch the Filter click and sync up the values of the two dropdowns. I chose a PHP-only solution that I’ll describe now.

How to Fix It

You can take advantage of the ability to turn HTML inputs into arrays of values, and then filter the array to get rid of any undefined values. Here’s the code:

    function add_course_section_filter() {
        if ( isset( $_GET[ 'course_section' ]) ) {
            $section = $_GET[ 'course_section' ];
            $section = !empty( $section[ 0 ] ) ? $section[ 0 ] : $section[ 1 ];
        } else {
            $section = -1;
        }
        echo ' <select name="course_section[]" style="float:none;"><option value="">Course Section...</option>';
        for ( $i = 1; $i <= 3; ++$i ) {
            $selected = $i == $section ? ' selected="selected"' : '';
            echo '<option value="' . $i . '"' . $selected . '>Section ' . $i . '</option>';
        }
        echo '</select>';
        echo '<input type="submit" class="button" value="Filter">';
    }
    add_action( 'restrict_manage_users', 'add_course_section_filter' );

    function filter_users_by_course_section( $query ) {
        global $pagenow;

        if ( is_admin() && 
             'users.php' == $pagenow && 
             isset( $_GET[ 'course_section' ] ) && 
             is_array( $_GET[ 'course_section' ] )
            ) {
            $section = $_GET[ 'course_section' ];
            $section = !empty( $section[ 0 ] ) ? $section[ 0 ] : $section[ 1 ];
            $meta_query = array(
                array(
                    'key' => 'course_section',
                    'value' => $section
                )
            );
            $query->set( 'meta_key', 'course_section' );
            $query->set( 'meta_query', $meta_query );
        }
    }
    add_filter( 'pre_get_users', 'filter_users_by_course_section' );

Bonus: PHP 7 Refactor

Since I’m excited about PHP 7, in case you’re running WP on a PHP 7 server, here’s a shorter, sexier version using the null coalescing operator ??:

function add_course_section_filter() {
    $section = $_GET[ 'course_section' ][ 0 ] ?? $_GET[ 'course_section' ][ 1 ] ?? -1;
    echo ' <select name="course_section[]" style="float:none;"><option value="">Course Section...</option>';
    for ( $i = 1; $i <= 3; ++$i ) {
        $selected = $i == $section ? ' selected="selected"' : '';
        echo '<option value="' . $i . '"' . $selected . '>Section ' . $i . '</option>';
    }
    echo '</select>';
    echo '<input type="submit" class="button" value="Filter">';
}
add_action( 'restrict_manage_users', 'add_course_section_filter' );

function filter_users_by_course_section( $query ) {
    global $pagenow;

    if ( is_admin() && 'users.php' == $pagenow) {
        $section = $_GET[ 'course_section' ][ 0 ] ?? $_GET[ 'course_section' ][ 1 ] ?? null;
        if ( null !== $section ) {
            $meta_query = array(
                array(
                    'key' => 'course_section',
                    'value' => $section
                )
            );
            $query->set( 'meta_key', 'course_section' );
            $query->set( 'meta_query', $meta_query );
        }
    }
}
add_filter( 'pre_get_users', 'filter_users_by_course_section' );

Enjoy!

Leave a Comment