Dominando WP_User_Query: construir un directorio de miembros escalable en WordPress

Dominando WP_User_Query: construir un directorio de miembros escalable en WordPress

Última verificación: 22 de septiembre de 2026
9 min de lectura
Guía
Desarrollador full-stack
Core Web Vitals

WP_Query se lleva la mayoría de los tutoriales. WP_User_Query es lo que realmente ejecutan los sitios de membresía, los directorios de personal y las plataformas comunitarias.

Si solo necesita una cuadrícula estática de “Nuestro equipo”, una consulta ligera en la plantilla gana a otro plugin de membresía que carga sus propias plantillas, rutas REST y CSS en cada petición. Esta guía se centra en ese problema: filtrar usuarios con role__in, meta_query y paginación sin quemar la base de datos ni filtrar nombres de login.

Referencia oficial: WP_User_Query en developer.wordpress.org.

#get_users() vs WP_User_Query

get_users() es un wrapper fino sobre WP_User_Query. Acepta el mismo array de argumentos y devuelve un array de objetos WP_User (o subconjuntos de campos cuando pasa fields).

Use get_users() cuando quiera una lista corta y nada más: por ejemplo cinco editores para un widget de firmas.

Use WP_User_Query cuando necesite:

  • get_results() más get_total() para directorios paginados
  • inspección de $query->request mientras depura SQL
  • la misma forma de argumentos que get_users(), pero con ciclo de vida explícito del objeto de consulta

En ambas APIs el array de argumentos es el contrato. Todo lo que sigue funciona con cualquiera de las dos formas; los ejemplos usan WP_User_Query porque los directorios necesitan totales.

#Construir una página de equipo con role__in

Editores y autores, ordenados por nombre para mostrar, doce por página:

$paged = max( 1, (int) get_query_var( 'paged', 1 ) );

$args = [
	'role__in' => [ 'editor', 'author' ],
	'orderby'  => 'display_name',
	'order'    => 'ASC',
	'number'   => 12,
	'paged'    => $paged,
	'fields'   => [ 'ID', 'display_name' ],
];

$user_query = new WP_User_Query( $args );
$results    = $user_query->get_results();

if ( ! empty( $results ) ) {
	echo '<div class="team-grid">';
	foreach ( $results as $user ) {
		$avatar = get_avatar( $user->ID, 128 );
		$name   = esc_html( $user->display_name );
		$bio    = esc_html( get_user_meta( $user->ID, 'description', true ) );

		echo "<article class='team-member'>
			<figure>{$avatar}</figure>
			<h3>{$name}</h3>
			<p>{$bio}</p>
		</article>";
	}
	echo '</div>';
}

Notas que importan en producción:

  • role__in coincide con cualquiera de los roles listados. Prefiéralo a varias consultas con role.
  • fields devuelve filas stdClass en lugar de objetos WP_User completos, lo que recorta memoria cuando solo necesita IDs y nombres.
  • Las biografías siguen necesitando una lectura de meta. Si la cuadrícula es caliente, cachee el HTML renderizado o precalcule una fila de directorio en una tabla personalizada.

#Paginación sin cargar todos los usuarios

Los argumentos de paginación son number (tamaño de página) y paged (base 1). Tras la consulta, get_total() es el conteo de coincidencias en todas las páginas, no solo en la actual.

$per_page = 24;
$paged    = max( 1, (int) ( $_GET['udir_page'] ?? 1 ) );

$query = new WP_User_Query(
	[
		'role__in' => [ 'subscriber', 'contributor' ],
		'number'   => $per_page,
		'paged'    => $paged,
		'fields'   => 'ID',
		'orderby'  => 'registered',
		'order'    => 'DESC',
	]
);

$total_users = (int) $query->get_total();
$total_pages = (int) ceil( $total_users / $per_page );
$user_ids    = $query->get_results();

// Construya enlaces con add_query_arg( 'udir_page', $n ) y escape las URLs.

Antipatrones a evitar:

  • Pedir 'number' => -1 (u omitirlo) y recortar con array_slice en PHP
  • Lanzar una segunda consulta completa solo para contar filas cuando get_total() ya hizo el trabajo
  • Meter valores no confiables de $_GET en orderby sin una lista blanca estricta

En directorios públicos use una query var dedicada (como arriba) para no chocar con el bucle principal paged del blog.

#Filtrado avanzado con meta_query

meta_query es donde los directorios se vuelven útiles - y donde aparece el coste de MySQL. Ejemplo: suscriptores en Madrid, solo perfiles públicos, con PHP listado en un meta de habilidades.

$args = [
	'role'       => 'subscriber',
	'number'     => 20,
	'paged'      => 1,
	'fields'     => [ 'ID', 'display_name' ],
	'meta_query' => [
		'relation' => 'AND',
		[
			'key'     => 'city',
			'value'   => 'Madrid',
			'compare' => '=',
		],
		[
			'key'     => 'is_public_profile',
			'value'   => '1',
			'compare' => '=',
		],
		[
			'key'     => 'skills',
			'value'   => 'PHP',
			'compare' => 'LIKE', // funciona con arrays serializados; caro a escala
		],
	],
];

$directory = new WP_User_Query( $args );

Reglas prácticas para meta_query sobre usuarios:

  • Prefiera compares exactos = sobre claves dedicadas (city, is_public_profile) frente a LIKE sobre blobs serializados.
  • Guarde habilidades multivalor como metas booleanas separadas (skill_php = 1) si las filtra a menudo. Así OR/AND se convierten en igualdades amigables con índices.
  • Combine relation con cuidado. Los grupos anidados son válidos, pero cada cláusula es otro join sobre wp_usermeta.
  • Empareje siempre los filtros públicos con un flag de visibilidad para que los perfiles privados no aparezcan en listados no autenticados.

#Escollos de rendimiento

Las consultas de usuarios parecen baratas hasta que el directorio es público y filtrado.

#Limitar los campos devueltos

Por defecto WordPress hidrata más datos de los que necesita una cuadrícula de tarjetas. Pase fields pronto:

$args = [
	'role'   => 'subscriber',
	'number' => 100,
	'fields' => [ 'ID', 'display_name', 'user_email' ],
];

Quite user_email de las plantillas públicas. Resérvelo solo para herramientas de personal detrás de comprobaciones de capacidades.

#Contar sin hidratar perfiles

$query = new WP_User_Query(
	[
		'role'   => 'subscriber',
		'fields' => 'ID',
	]
);
$count = $query->get_total();

Para totales por rol sin filtros, count_users() es más barato porque usa los conteos de rol que WordPress ya mantiene.

#Cachear conjuntos de resultados estables

Si el directorio cambia poco, guarde IDs (o marcado renderizado) en un transient con clave por hash de filtros. Invalide el transient cuando se actualice el meta de perfil vía updated_user_meta para las claves que filtra. No cachee HTML que incluya campos condicionados por capacidades para visitantes anónimos.

#Saber cuándo MySQL es el índice equivocado

Los joins sobre wp_usermeta no escalan como un motor de búsqueda. Cuando los filtros se multiplican (ciudad + especialidad + idioma + disponibilidad) y la audiencia es grande, mueva la proyección buscable a Elasticsearch (ElasticPress o un índice propio) o a una tabla de directorio dedicada con índices reales. Conserve WP_User_Query para la hidratación final de una lista pequeña de IDs.

#Evitar lecturas N+1 de avatar y meta

Dentro del bucle, get_avatar() y llamadas repetidas a get_user_meta() se multiplican. Precargue claves conocidas con una sola consulta a wp_usermeta para el conjunto de IDs de la página, o use una fila de directorio desnormalizada escrita al guardar el perfil.

#Seguridad: enumeración de usuarios en directorios

Un directorio de miembros es una lista pública de cuentas. Trátelo como tal.

  1. Nunca imprima user_login en el marcado del frontend. Use display_name o un apodo público dedicado.
  2. Nunca imprima user_email salvo que el visitante esté autenticado y autorizado a verlo.
  3. Los archivos de autor (/?author=1) siguen filtrando logins en muchos sitios. Si el producto no necesita archivos de autor públicos, rediríjalos:
add_action(
	'template_redirect',
	static function () {
		if ( is_author() ) {
			wp_safe_redirect( home_url( '/' ), 301 );
			exit;
		}
	}
);
  1. Escape todo lo que salga de PHP: esc_html(), esc_url(), esc_attr() en atributos.
  2. Condicione por capacidades los directorios solo-admin (list_users) en lugar de confiar en la oscuridad de la URL.

#Montar una plantilla de directorio

Un directorio público durable suele tener cuatro capas:

  1. Sanitización de entrada - claves de orden en lista blanca, página entera, slug de ciudad normalizado.
  2. Consulta - role__in / meta_query / fields / number / paged.
  3. Presentación - tarjetas solo con IDs y campos de visualización seguros.
  4. Invalidación de cache - ligada a las claves meta que afectan a la visibilidad.

Deje la UI del plugin para el alta de cuentas y la mensajería si necesita esas funciones. Deje el listado y el filtrado en su tema o en un mu-plugin pequeño que controle usted. Esa separación es lo que mantiene el directorio rápido cuando marketing añade otro filtro el trimestre siguiente.

#Multisite y tablas de usuarios compartidas

En una red WordPress multisite la tabla de usuarios es compartida. WP_User_Query sigue funcionando, pero los filtros de rol son por sitio. Un usuario que es Editor en el sitio A puede no tener rol en el sitio B. Para un directorio de personas a escala de red:

  • Prefiera blog_id cuando deba acotar roles a un solo sitio.
  • No asuma que role__in => array( 'author' ) significa las mismas personas en cada blog.
  • Para listas de personal entre sitios, guarde un flag de meta de red (por ejemplo wpp_show_in_directory) y consulte eso en lugar de roles solos.

Las copias de staging de multisite a menudo recortan usuarios. Mida el coste de la consulta sobre un dump que coincida con la cardinalidad de producción, o su plan de meta_query parecerá fino hasta el día del lanzamiento.

#Probar el SQL que realmente envía

Antes de fusionar:

  1. Active Query Monitor en staging.
  2. Abra la URL del directorio con la combinación de filtros más pesada que use marketing.
  3. Confirme una consulta primaria de usuarios, no una consulta por tarjeta para el meta.
  4. Guarde $query->request en las notas del PR para que la siguiente persona detecte regresiones.

Si el directorio debe soportar búsqueda de texto libre sobre nombres para mostrar, use un índice de búsqueda dedicado o un argumento search bien acotado en WP_User_Query. No haga LIKE sobre user_email en un endpoint público. Registre las peticiones lentas del directorio con el payload de filtros para poder reproducirlas después.

¿Necesita revisar un directorio WordPress o una consulta de membresía antes de producción? Contacte con WPPoland.

#Lista de comprobación antes de publicar

  1. Use fields para no hidratar objetos de usuario completos en cuadrículas de tarjetas.
  2. Paginate con number + paged y confíe en get_total() para la matemática de páginas.
  3. Prefiera claves meta exactas frente a LIKE sobre arrays de habilidades serializados.
  4. Cachee listas de IDs o marcado cuando los filtros sean estables; invalide al escribir perfiles.
  5. Renderice solo display_name; oculte logins y correos del HTML público.
  6. Mida el SQL ($query->request) en staging con volumen realista de usuarios y meta antes del lanzamiento.

Los usuarios son objetivos de consulta de primera clase en WordPress. Consúltelos con la misma disciplina que ya aplica a WP_Query.

Siguiente paso

Transforma el artículo en una implementación real

Este bloque refuerza el enlazado interno y lleva al lector al siguiente paso más útil dentro de la arquitectura del sitio.

Cluster relacionado

Explora otros servicios WordPress y base de conocimiento

Refuerza tu negocio con soporte técnico profesional en áreas clave del ecosistema WordPress.

FAQ del artículo

Preguntas frecuentes

Respuestas prácticas para aplicar el tema en la ejecución real.

SEO-readyGEO-readyAEO-ready4 Q&A
¿Cuándo debo usar WP_User_Query en lugar de get_users?#
Use get_users() para listas simples en las que solo necesita un array de objetos WP_User. Pase a WP_User_Query cuando necesite orderby fino, meta_query, control de fields, totales de paginación con get_total(), o acceso directo al SQL que produjo la consulta.
¿Cómo pagino un directorio de miembros de forma segura?#
Pase number (tamaño de página) y paged (índice de página base 1) en el array de argumentos, y use get_total() para el conteo total de coincidencias. Nunca cargue todos los usuarios en memoria y los recorte con array_slice en PHP: ese patrón se derrumba bajo tráfico real.
¿Por qué meta_query es lento en directorios grandes?#
Cada cláusula de meta_query hace un join sobre wp_usermeta. Los compares LIKE y los arrays de habilidades serializados fuerzan escaneos de tabla. Prefiera claves exactas, mantenga flags de directorio público indexados en una tabla personalizada, o desvíe la búsqueda a Elasticsearch cuando el directorio crezca más allá de lo que los joins de MySQL manejan con holgura.
¿Qué no debo imprimir nunca en un listado público de miembros?#
Nunca muestre user_login ni user_email en un directorio público. Renderice display_name (o un meta de apodo público dedicado) y reserve el correo detrás de comprobaciones de capacidades o vistas de intranet autenticadas.

¿Necesitas un FAQ adaptado a tu sector y mercado? Preparamos una versión alineada con tus objetivos de negocio.

Hablemos

Artículos Relacionados