How To Add A Custom Filter To WordPress Users List

I was working on a membership site which had grown to a few thousand users. We had two sets of users, the individual type of which were set in the user meta.

Basically, we wanted to add this...

So how do you add a custom filter to the WordPress users list?

  1. Create the filter dropdown and button inside a function
  2. Add that function to the 'restrict_manage_users' action
  3. Add a new filter for 'pre_get_users'
  4. Look at the $_GET parameters to see which button was clicked
  5. Change the meta query accordingly

I'll walk through those steps below, but here's the code I used if you just want that...

/*** Sort and Filter Users ***/
add_action('restrict_manage_users', 'filter_by_job_role');

function filter_by_job_role($which)
{
 // template for filtering
 $st = '<select name="job_role_%s" style="float:none;margin-left:10px;">
    <option value="">%s</option>%s</select>';

 // generate options
 $options = '<option value="job_seeker">Seeker</option>
    <option value="job_lister">Hirer</option>';
 
 // combine template and options
 $select = sprintf( $st, $which, __( 'Job Role...' ), $options );

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

add_filter('pre_get_users', 'filter_users_by_job_role_section');

function filter_users_by_job_role_section($query)
{
 global $pagenow;
 if (is_admin() && 'users.php' == $pagenow) {
  // figure out which button was clicked. The $which in filter_by_job_role()
  $top = $_GET['job_role_top'] ? $_GET['job_role_top'] : null;
  $bottom = $_GET['job_role_bottom'] ? $_GET['job_role_bottom'] : null;
  if (!empty($top) OR !empty($bottom))
  {
   $section = !empty($top) ? $top : $bottom;
   
   // change the meta query based on which option was chosen
   $meta_query = array (array (
      'key' => 'membership_user_role',
      'value' => $section,
      'compare' => 'LIKE'
   ));
   $query->set('meta_query', $meta_query);
  }
 }
}

You put that code inside the functions.php file of your child theme. Or you could add it to a WordPress plugin that you're building.

Let's go through the code.

Create the filter dropdown and button inside a function

We create a function which will have "top" or "bottom" passed through to it, depending on whether the user tries to filter the users with the buttons above or below the user list.

function filter_by_job_role($which)
{
 // template for filtering
 $st = '<select name="job_role_%s" style="float:none;margin-left:10px;">
    <option value="">%s</option>%s</select>';

 // generate options
 $options = '<option value="job_seeker">Seeker</option>
    <option value="job_lister">Hirer</option>';
 
 // combine template and options
 $select = sprintf( $st, $which, __( 'Job Role...' ), $options );

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

$which will be either "top" or "bottom".

We setup a template using $st to make it easier to see and change our options. The %s parts are going to be strings that we'll pass into the template later.

I added style="float:none;margin-left:10px;" just for the visuals. You can leave that out.

$options contains the actual options we'll use. To make your code simpler and more robust, set the value to be whatever the meta values you have for sorting the users.

If for some reason you can't set the values here to your meta values, you'll have to conditionally filter the meta query in the other function.

The $select variable does most of the heavy lifting, combining your template with the other parts. The way it does this is:

  • Pulls in the $st template
  • Puts the value of $which into the first %s of $st
  • Adds "Job Role..." as the first, valueless option (the second %s of $st)
  • Adds the rest of the options from $options to the third %s of $st

The rest of the code in the function outputs the dropdown you just created and a button called "Filter" that triggers the selection.

Add that function to the 'restrict_manage_users' action

Once you've done all that, hook that function into the 'restrict_manage_users' action with the usual add_action function like so:

add_action('restrict_manage_users', 'filter_by_job_role');

Add a new filter for 'pre_get_users'

We create a function called filter_users_by_job_role_section to filter the users and hook it into pre_get_users.

add_filter('pre_get_users', 'filter_users_by_job_role_section');

function filter_users_by_job_role_section($query)
{
 global $pagenow;
 if (is_admin() && 'users.php' == $pagenow) {
  // put the filtering code in here
 }
}

We bring in the WordPress global $pagenow so we can check which page we're on. We only want to execute it on the users.php page.

We also double check that the user is logged in with Administrator privileges, which is just good practice for executing code like this.

Look at the $_GET parameters to see which button was clicked

There are a number of ways you can do this. PHP 7.0+ has some fancy filtering options with nested functions, but I went with simple and clear and backwards compatible:

$top = $_GET['job_role_top'] ? $_GET['job_role_top'] : null;
$bottom = $_GET['job_role_bottom'] ? $_GET['job_role_bottom'] : null;
if (!empty($top) OR !empty($bottom))
{
 $section = !empty($top) ? $top : $bottom;
}

This code essentially pulls in the values of the top and bottom buttons, checks that one of them has a value and then attaches that value to $section.

Worth noting is that with the way the buttons are setup, you're not going to have a situation where both top and bottom buttons have a value. But even if you did, it would go with the value of the top button.

Change the meta query accordingly

Because we're using add_filter rather than add_action here, we don't actually have to return anything. We can just make a change to the $query, which was passed into the function initially. We do that with this code:

$meta_query = array (array (
  'key' => 'membership_user_role',
  'value' => $section,
  'compare' => 'LIKE'
));
$query->set('meta_query', $meta_query);

I put this inside the if (!empty($top) OR !empty($bottom)), but if you have other code that needs executing, then you might put it elsewhere.

The $meta_query has to be an array of an array because you can use this query for filtering on multiple meta keys. I'll go into that later in this post.

The key is the meta_key in the wp_usermeta table that you want to filter on.

The value is which meta_value you want to show.

In our setup, each user has a meta_key of membership_user_role. Job Seekers have the meta_value of job_seeker, while Hirers have the meta_value of job_lister.

When you use this code to filter your own users, you'll change 
membership_user_role
to whatever you use for filtering.

The compare allows you to do a range of comparisons. We only need LIKE, but you might use:

  • LIKE
  • NOT LIKE
  • IN
  • NOT IN
  • EXISTS
  • NOT EXISTS

You can also use various operators and even some regex. See the WP_Meta_Query reference for more ideas and implementation.

Finally, we set the meta_query in the $query object that was passed into the function initially. This basically takes the place of a return.

How To Filter Users By Multiple Meta Keys

As mentioned above, we can use these exact same steps to filter our users by multiple meta key / value pairs.

I actually ended up doing this for this project, because I wanted to further filter by paid or unpaid status, which was stored in another meta key for these users.

In our implementation, when a Hirer pays, we add the value '1' to joblister_paid on that user's meta. If they haven't paid, or they stopped payment, we don't delete their account (they can), we just set the value of joblister_paid to zero or blank.

Essentially, a value of 1 means paid, and anything else is unpaid. So I needed to do a negative comparison.

The query I used for unpaid Hirers was:

$meta_query = array (
 'relation' => 'AND',
 array (
  'key' => 'jobboard_user_role',
  'value' => 'job_lister',
  'compare' => 'LIKE'
 ),
 array (
  'key' => 'joblister_paid',
  'value' => '1',
  'compare' => 'NOT LIKE'
 )
);

Technically, I didn't need to include "relation" because it defaults to "AND", but it's good for clarity of code. You could also use "OR". This is for comparing the two queries.

To figure out which $meta_query to use, I actually had a couple of extra options in the filter_by_job_role function. Then I used a switch on the $section variable to sort out which query I needed.

It basically looked like this:

switch ($section)
{
 case "hirerpaid":
  $meta_query = array (
   'relation' => 'AND',
   array (
    'key' => 'jobboard_user_role',
    'value' => 'job_lister',
    'compare' => 'LIKE'
   ),
   array (
    'key' => 'joblister_paid',
    'value' => '1',
    'compare' => '='
   )
  );
  break;
 case "hirerunpaid":
  $meta_query = array (
   'relation' => 'AND',
   array (
    'key' => 'jobboard_user_role',
    'value' => 'job_lister',
    'compare' => 'LIKE'
   ),
   array (
    'key' => 'joblister_paid',
    'value' => '1',
    'compare' => 'NOT LIKE'
   )
  );
  break;
 default:
  $meta_query = array (array (
   'key' => 'jobboard_user_role',
   'value' => $section,
   'compare' => 'LIKE'
  ));
  break;
}
$query->set('meta_query', $meta_query);

You would use this code in place of the snippet we went through under the heading Change the meta query accordingly above.

Let me know what you thought of this guide. Did you use a different method to implement it?

Mike Haydon

Thanks for checking out my WordPress and coding tutorials. If you've found these tutorials useful, why not consider supporting my work?

Buy me a coffee

11 thoughts on “How To Add A Custom Filter To WordPress Users List”

  1. This is an excellent guide. I was having a hard time getting a working filter set up on a users.php page for a client, but was able to get it working by using this!

    Thanks very much for taking the time to put it together.

    Reply
  2. Hi,

    How can I alter this so that it searches for all users by first name, last name, full name, or city/province? Having these options instead of only a username search would be a GREAT help!

    Reply
  3. Hi,

    great manual!
    Just a small improvement:
    $top = ( isset($_GET['job_role_top']) ? $_GET['job_role_top'] : null );
    $bottom = ( isset($_GET['job_role_bottom']) ?$_GET['job_role_bottom'] : null );

    What about highlighting which filter is applied? Any hints how to inform the admin?

    Kind regards,
    Magic

    Reply
    • Good pickup. Thanks Magic. I've amended it accordingly.

      If you want to highlight which filter is being used, check for job_role_top or job_role_bottom in $_GET, then highlight accordingly.

      You could add it as a notice, or if you mean to highlight it in the select, you'd conditionally add the "selected" parameter to the $options setup.

      Reply
    • Hey Mike

      So if you look at https://www.w3schools.com/tags/att_option_selected.asp you'll see that they add the "selected" parameter to the option they want selected.

      I'd use the code from https://intelliwolf.com/how-to-add-a-custom-filter-to-wordpress-users-list/#which-button-clicked to figure out which option should be highlighted.

      Then use a loop in place of $options. It might look like:

      $option_values = array(
      'job_seeker' => 'Seeker',
      'job_lister' => 'Hirer'
      );
      $options = "";
      foreach ($option_values as $key => $value)
      {
      $selected = ($key == $section) ? "selected" : "";
      $options = '<option value="' . $key . '" ' . $selected . '>' . $value . '</option>';
      }

      Reply
      • Dear Mike,

        thank you for inspirations!
        So the final code could look like this:

        // this variable must be global, as it is used by two functions:
        $section = '';

        /*** Sort and Filter Users ***/
        add_action('restrict_manage_users', 'filter_users_by_job_role_section');
        function filter_users_by_job_role_section($which)
        {
        global $section;

        // template for filtering
        $st = '%s%s';

        // generate options
        $option_values = array (
        'job_seeker' => 'Seeker',
        'job_lister' => 'Hirer');

        $options = '';
        foreach ($option_values as $key => $value) {
        $selected = ($key == $section) ? " selected" : "";
        $options .= '' . $value . '';
        }

        // combine template and options
        $select = sprintf( $st, $which, __( 'Job Role...' ), $options );

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

        add_filter('pre_get_users', 'filter_users_by_job_role_section');

        function filter_users_by_job_role_section($query)
        {
        global $pagenow;
        global $section;
        ...

        Happy New Year,
        Mike

        Reply
        • Hey Mike

          Thanks to WordPress stripping, your filter didn't come through, but I'll assume it was the same as I had in the article.

          I also realised it stripped out my $options code in my previous comment, so you may want to look over that again. Sorry.

          I hate using globals if I don't have to. If you're using it more than once, you're better to make it into a function, passing $_GET into it.

          You might do:

          function which_user_filter_button_clicked($data)
          {
          if(empty($data)) return false;
          $top = $data['job_role_top'] ? $data['job_role_top'] : null;
          $bottom = $data['job_role_bottom'] ? $data['job_role_bottom'] : null;

          if (!empty($top) OR !empty($bottom))
          {
          return !empty($top) ? $top : $bottom;
          }
          return false;
          }

          Then call it with
          $section = which_user_filter_button_clicked($_GET);
          if (!$section) return false;

          Happy New Year :)

Leave a Comment