Querying Users

WP_User_Query with role__in, capability filters, meta queries on users, and pagination that actually reports a total.

Syntax$query = new WP_User_Query( array $args ); $users = $query->get_results();

WP_User_Query is to wp_users what WP_Query is to wp_posts: you pass an argument array, it builds the SQL, joins wp_usermeta when needed and hands back objects. Construct it and call get_results(), or use get_users( $args ), which is shorthand for exactly that pair.

Roles and capabilities

role takes a name or list that a user must match in full. role__in and role__not_in (WordPress 4.4) take arrays and mean "any of" and "none of". Since WordPress 5.9 there are capability, capability__in and capability__not_in, which match on the capability itself — but only capabilities stored in the database, so anything granted through the map_meta_cap filter is invisible to them.

Meta and pagination

meta_query is the same WP_Meta_Query structure you use on posts, running against wp_usermeta. Pagination differs from WP_Query in one important way: number defaults to -1, meaning every user on the site. Always set it. paged pages through the results, offset skips a fixed count, and count_total (default true) controls whether get_total() has an answer.

Shape of the results

fields defaults to 'all' and gives WP_User objects. Pass a single field name such as 'ID' for a flat array, or an array of column names for lightweight stdClass rows.

Example

Example · php
<?php
$per_page = 20;
$paged    = max( 1, (int) get_query_var( 'paged' ) );

$query = new WP_User_Query( array(
	// role__in means "any of these" (WordPress 4.4+). Plain 'role' would
	// require the user to hold every role listed.
	'role__in'   => array( 'author', 'editor' ),

	// Same WP_Meta_Query syntax as posts, run against wp_usermeta.
	'meta_query' => array(
		'relation' => 'AND',
		array(
			'key'   => 'acme_region',
			'value' => 'emea',
		),
		array(
			'key'     => 'acme_last_seen',
			'value'   => gmdate( 'Y-m-d', strtotime( '-30 days' ) ),
			'compare' => '>=',
			'type'    => 'DATE',
		),
	),

	'number'      => $per_page, // default is -1, i.e. every user on the site
	'paged'       => $paged,
	'orderby'     => 'display_name',
	'order'       => 'ASC',
	'count_total' => true,      // required for get_total() to return anything
) );

$users = $query->get_results();          // WP_User objects ('fields' => 'all')
$total = $query->get_total();            // matching rows, ignoring number/paged
$pages = (int) ceil( $total / $per_page );

When to use it

  • Build a paginated staff directory filtered by a custom user meta field.
  • Email every user who holds a given capability, whatever their role.
  • Export subscribers who registered in a date range for a mailing list.
  • Hide service accounts from the admin Users screen.

More examples

get_users() as shorthand, and the ID-only form

Use get_users() for the common case where you just want the rows. Switch to 'fields' => 'ID' whenever the users are an intermediate step: hydrating hundreds of WP_User objects primes the meta cache for every one of them, which is wasted work if you only need their IDs.

Example · php
// These two are the same query. get_users() builds the object and returns
// get_results() for you — but it also forces count_total to false.
$users = get_users( array( 'role' => 'editor', 'number' => 50 ) );

$q     = new WP_User_Query( array( 'role' => 'editor', 'number' => 50 ) );
$users = $q->get_results();

// 'fields' => 'ID' returns a flat array of ints and skips hydrating
// WP_User objects — much cheaper when you only need to feed another query.
$editor_ids = get_users( array(
	'role'   => 'editor',
	'fields' => 'ID',
	'number' => 200,
) );

$posts = new WP_Query( array(
	'author__in'     => $editor_ids,
	'posts_per_page' => 10,
) );

Finding users by capability rather than role

The right query when you want everyone who can do something, not everyone with a particular job title — notifying whoever can approve orders, for example. It is more robust than a role list because it survives a site owner creating their own role.

Example · php
// WordPress 5.9+. Matches on the capability itself, so it also finds users
// who hold it through a custom role or a per-user grant.
$q = new WP_User_Query( array(
	'capability__in' => array( 'acme_manage_orders', 'manage_woocommerce' ),
	'number'         => 100,
	'fields'         => array( 'ID', 'display_name', 'user_email' ), // stdClass rows
) );

foreach ( $q->get_results() as $row ) {
	// $row is stdClass, not WP_User, because 'fields' is an array.
	wp_mail( $row->user_email, 'New order queue', 'Hello ' . $row->display_name );
}

// Caveat: this reads the stored capabilities meta. A capability granted
// dynamically through the map_meta_cap filter will not be matched here.

Excluding users from the admin list table with pre_user_query

The escape hatch for conditions the argument array cannot express, and the way to alter queries WordPress core runs on your behalf, such as the admin user list. Everything you append must go through $wpdb->prepare() — you are writing raw SQL at this point, with no sanitising layer left below you.

Example · php
/**
 * Hide service accounts from Users -> All Users for everyone except
 * network administrators. pre_user_query fires after parsing, before the
 * SQL runs, and receives the WP_User_Query object by reference.
 *
 * @param WP_User_Query $query
 */
add_action( 'pre_user_query', static function ( $query ): void {
	if ( ! is_admin() || ! $query->get( 'blog_id' ) ) {
		return;
	}
	if ( current_user_can( 'manage_network_users' ) ) {
		return;
	}

	global $wpdb;
	$hidden = array( 'acme_bot', 'acme_sync' );
	$in     = implode( ',', array_fill( 0, count( $hidden ), '%s' ) );

	$query->query_where .= $wpdb->prepare(
		" AND {$wpdb->users}.user_login NOT IN ( $in )",
		$hidden
	);
} );

Discussion

  • Be the first to comment on this lesson.