WP_Query får de fleste tutorialene. WP_User_Query er det medlemsportaler, ansattkataloger og fellesskapssider faktisk kjører på. Trenger du bare et «Vårt team»-rutenett, slår en lett spørring i temaet en medlemsplugin som drar med egne maler, REST-ruter og CSS på hver forespørsel.
Denne guiden holder seg til det problemet: filtrere brukere med role__in, meta_query og paginering uten å steke databasen eller lekke påloggingsnavn. Eksemplene er skrevet for PHP-temaer og mu-plugins, ikke for page-builder-shortcodes. Offisiell referanse: WP_User_Query og get_users(). Alle standardverdier og all oppførsel beskrevet under er sjekket mot kildekoden til klassen i wordpress-develop 12. oktober 2026.
Forskjellen på get_users() og WP_User_Query
get_users() er en tynn wrapper rundt WP_User_Query. Den tar samme argument-array og returnerer WP_User-objekter (eller feltutvalg når du setter fields).
Bruk get_users() når du vil ha en kort liste og ikke noe mer - for eksempel fem redaktører til en byline-widget.
Bruk WP_User_Query når du trenger:
get_results()plussget_total()til paginerte kataloger- inspeksjon av
$query->requestmens du debugger SQL - samme args-form som
get_users(), men med eksplisitt spørringsobjekt
Én forskjell i oppførsel er verdt å kjenne. get_users() setter count_total til false før spørringen bygges, så den kjører aldri den ekstra tellingen og har ingen total å gi deg. Det er riktig standard for en widget og feil standard for en paginert katalog.
Argument-arrayen er kontrakten. Eksemplene under bruker WP_User_Query fordi kataloger trenger totaler. Begge API-ene leser samme dokumentasjon; forskjellen er kontrollen du får over resultatet etter spørringen.
Hva WP_User_Query returnerer med standardargumenter
Standardverdiene avgjør mesteparten av kostnaden ved en slurvete spørring:
| Argument | Standard | Hva det betyr i praksis |
|---|---|---|
number | ingen grense | Alle brukere som matcher kommer tilbake. Docblocken ber om forsiktighet med -1 (alle) på større nettsteder. |
paged | 1 | Brukes bare når number er større enn null. |
orderby | login | Sortering på user_login, sjelden det en offentlig liste vil ha. |
fields | all | Fulle WP_User-objekter, med all brukermeta forhåndslastet via cache_users(). |
count_total | true | Legger til SQL_CALC_FOUND_ROWS og kjører en ekstra spørring SELECT FOUND_ROWS(). |
blog_id | gjeldende nettsted | Rolle- og medlemskapsfiltre gjelder bare gjeldende nettsted. |
cache_results | true | Resultatene lagres i objektcachen (lagt til i WordPress 6.3). |
To konsekvenser følger. En spørring uten number laster alle brukerne på et nettsted med mange kontoer. Og en spørring som aldri paginerer bør sende 'count_total' => false, fordi docblocken sier at det gir bedre ytelse når paginering ikke trengs.
Klassen nekter også å kjøre for tidlig: siden WordPress 6.1.1 utløser en brukerspørring før hooken plugins_loaded et _doing_it_wrong()-varsel.
WP_User_Query med role__in for en teamside
Redaktører og forfattere, sortert etter visningsnavn, tolv per side:
$paged = max( 1, absint( $_GET['team_page'] ?? 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 ) ) {
// Én spørring for metaen til alle tolv brukerne i stedet for tolv spørringer.
update_meta_cache( 'user', wp_list_pluck( $results, 'ID' ) );
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>';
}I produksjon:
role__inmatcher hvilken som helst av rollene. Foretrekk det fremfor flere spørringer medrole.fieldsgirstdClass-rader i stedet for fulleWP_User-objekter.- Med en
fields-array forhåndslaster ikke WordPress brukermeta, så kallet tilupdate_meta_cache()over er det som holder bio-oppslagene på én spørring. Medfields => 'all'gjør klassen dette selv viacache_users(). - Sidetallet kommer fra en egen parameter,
team_page. På en statisk side bruker hovedspørringenpage, ikkepaged, såget_query_var( 'paged' )der returnerer stille standardverdien.
Slik virker filtrene role, role__in og capability
De tre rolleargumentene ser like ut og oppfører seg ulikt:
roletar en streng eller en array, og arrayen er en OG-liste: brukeren må ha hver rolle i listen.'role' => [ 'editor', 'author' ]gir nesten ingen treff på et vanlig nettsted.role__iner ELLER-listen: minst én av rollene.role__not_inutelater brukere som har en av rollene i listen.
Under panseret rører ingen av dem en rolletabell, for WordPress har ingen. Rollene ligger i en serialisert array i brukermeta-raden {prefix}capabilities, og hver rolleklausul blir en sammenligning LIKE '%"editor"%' mot den raden. Et rollefilter er altså allerede en join mot wp_usermeta før du har lagt til én eneste meta_query.
Siden WordPress 5.9 finnes også capability, capability__in og capability__not_in. De oversetter kapasiteter til rollene som gir dem, pluss kapasiteter lagt direkte på en bruker. Docblocken advarer om at de ikke virker for kapasiteter som ikke er lagret i databasen eller som filtreres via map_meta_cap. Bruk dem som snarvei for primitive kapasiteter som edit_posts, ikke som tilgangskontroll.
Slik paginerer du WP_User_Query med get_total()
Args: number (sidestørrelse) og paged (1-basert). get_total() er antall treff på tvers av sider - ikke bare inneværende side.
$per_page = 24;
$paged = max( 1, absint( $_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();
// Bygg lenker med add_query_arg( 'udir_page', $n ) og escape URL-ene.Unngå:
'number' => -1ogarray_slicei PHP- ekstra full spørring bare for å telle når
get_total()allerede finnes - usikre
$_GET-verdier rett inn iorderbyuten allow-list
To detaljer fra kildekoden overrasker mange. Sender du offset, vinner den: klassen bygger LIMIT offset, number og ignorerer paged. Og med fields => 'ID' er resultatet en flat array av ID-strenger fra $wpdb->get_col(), ikke objekter, så cast før strenge sammenligninger.
På offentlige kataloger: egen query var (udir_page) så du ikke kolliderer med bloggens paged.
Avansert filtrering med meta_query
Eksempel: abonnenter i Oslo, offentlig profil, ferdighet «PHP» i et meta-felt for ferdigheter.
$args = [
'role' => 'subscriber',
'number' => 20,
'paged' => 1,
'fields' => [ 'ID', 'display_name' ],
'meta_query' => [
'relation' => 'AND',
[
'key' => 'city',
'value' => 'Oslo',
'compare' => '=',
],
[
'key' => 'is_public_profile',
'value' => '1',
'compare' => '=',
],
[
'key' => 'skills',
'value' => 'PHP',
'compare' => 'LIKE', // virker på serialiserte arrays; dyrt i stor skala
],
],
];
$directory = new WP_User_Query( $args );Praktiske regler:
- Eksakt
=på egne nøkler (city,is_public_profile) slårLIKEpå serialiserte blobs. LIKE 'PHP'er et delstrengsøk og treffer derfor også en ferdighet som heterPHPUnit. Det er en riktighetsfeil, ikke bare et ytelsesproblem.- Lagre ofte brukte ferdigheter som booleans (
skill_php=1) hvis du filtrerer dem mye. Et alternativ er énskills-rad per verdi, skrevet medadd_user_meta( $user_id, 'skills', 'php' ), som tillater dupliserte nøkler, og filtrert med'compare' => '='eller'IN'. - Hver klausul er en join på
wp_usermeta- holdrelation-trær korte. - Kombiner alltid offentlige filtre med et synlighetsflagg, slik at private profiler aldri dukker opp for anonyme.
Hvorfor eksakte nøkler betyr noe, ser du i kjerneskjemaet. Tabellen wp_usermeta som wp-admin/includes/schema.php oppretter, har en primærnøkkel, en indeks på user_id og en prefiksindeks på meta_key. Det finnes ingen indeks på meta_value, som er en longtext-kolonne. MySQL kan avgrense joinen til radene for én nøkkel, men må lese hver av dem for å sammenligne verdier.
For svært store kataloger: vurder Elasticsearch (ElasticPress) eller egen oppslagstabell. Hold WP_User_Query til å hydrere en liten ID-liste.
Slik virker search og search_columns i WP_User_Query
Argumentet search er lett å bruke feil, fordi det ikke oppfører seg som et søkefelt.
- Uten jokertegn er det et eksakt treff.
'search' => 'kari'finner brukeren med login nøyaktigkari. Sett*først, sist eller begge steder ('*kari*') for å fåLIKEmed%. search_columnsgodtarID,user_login,user_email,user_url,user_nicenameogdisplay_name.IDsammenlignes alltid med=.- Er
search_columnstom, gjetter WordPress: et søkeord med@søker iuser_email, et tall iuser_loginogID, en URL iuser_url, og alt annet i login, URL, e-post, nicename og visningsnavn samtidig.
Den siste standarden er farlig i en offentlig katalog. En besøkende som skriver deler av en e-postadresse i søkefeltet ditt, kan bekrefte at adressen tilhører et medlem. Sett alltid kolonnene selv for offentlig søk:
$term = sanitize_text_field( wp_unslash( $_GET['q'] ?? '' ) );
$args = [
'role__in' => [ 'subscriber' ],
'search' => '*' . $term . '*',
'search_columns' => [ 'display_name' ],
'number' => 20,
'fields' => [ 'ID', 'display_name' ],
];Filteret user_search_columns lar en plugin endre kolonnelisten for alle brukersøk på nettstedet, så sjekk det når resultatene ser feil ut.
Raskere WP_User_Query med fields, telling og cache
Når katalogen er offentlig og filtrert, blir brukerspørringer dyre fort. Et ansatt-rutenett med tolv kort er ufarlig. Et «finn behandler»-søk med by, spesialitet og språk mot mange wp_usermeta-rader er en annen historie.
Slik begrenser du feltene i WP_User_Query
$args = [
'role' => 'subscriber',
'number' => 100,
'fields' => [ 'ID', 'display_name', 'user_email' ],
];Dropp user_email fra offentlige maler. Behold det bak current_user_can( 'list_users' ) i admin-verktøy, se roller og rettigheter.
Hold listen kort av en grunn til: når en fields-array nevner mer enn tre felt, slår WP_User_Query av sin egen resultatcache for den spørringen.
Slik teller du brukere uten å laste profiler
$query = new WP_User_Query(
[
'role' => 'subscriber',
'number' => 1,
'fields' => 'ID',
]
);
$count = $query->get_total();number => 1 holder resultatet på én ID, mens get_total() fortsatt rapporterer hele antallet.
Uten filtre finnes count_users(), men den er ikke gratis: det ligger ingen lagret rolleteller bak. Med standardstrategien time kjører den én aggregert spørring over alle {prefix}capabilities-rader med én LIKE per rolle, og docblocken beskriver den som CPU-krevende og egnet for rundt 10^7 brukere. Trenger du bare totalt antall kontoer, leser get_user_count() en lagret verdi, user_count. På nettsteder som wp_is_large_user_count() regner som store (over 10 000 brukere som standard), oppdateres verdien av en cron-hendelse to ganger i døgnet, så den kan henge etter.
Slik cacher du resultater fra WP_User_Query
Siden WordPress 6.3 cacher klassen selv. Med cache_results satt til true lagrer den resultat-ID-ene og totalen i objektcache-gruppen user-queries, med en MD5 av den genererte SQL-en som nøkkel. Oppføringen saltes med last-changed-tidsstempelet for users, og kjernen oppdaterer det på added_user_meta, updated_user_meta, deleted_user_meta, add_user_role, set_user_role og remove_user_role. Enhver profilendring invaliderer alle cachede brukerspørringer.
Denne cachen overlever bare mellom forespørsler når nettstedet har en persistent objektcache (Redis eller Memcached). Uten den varer den én sidevisning.
Legger du ditt eget lag oppå, for eksempel en transient med ferdig HTML, nøkle den på filter-hash og invalider den fra de samme hookene. Cache aldri HTML med kapasitetsstyrte felt for anonyme. Og hold hyppige meta-skrivinger ute av forespørselen: en plugin som oppdaterer en «sist sett»-meta på hver sidevisning, flytter users-tidsstempelet på hvert treff og etterlater den innebygde cachen uten noe å servere.
Når katalogen bør flyttes til Elasticsearch
Joins mot wp_usermeta skalerer ikke som en søkemotor. Når filtrene blir mange (by + spesialitet + språk + tilgjengelighet) og publikum stort, flytt den søkbare projeksjonen til Elasticsearch (ElasticPress eller egen indeks) eller en egen katalogtabell med ekte indekser. Behold WP_User_Query til den siste hydreringen av en kort ID-liste.
Klassen har en hook for akkurat dette. Returnerer filteret users_pre_query en array i stedet for null, hoppes SQL-en helt over; docblocken ber slike filtre sette egenskapen total_users så paginering fortsatt virker.
Slik unngår du N+1-kall for avatar og brukermeta
I løkken multipliserer get_avatar() og gjentatte get_user_meta(). Forhåndslast metaen for hele siden med update_meta_cache( 'user', $ids ), som henter alle meta-rader for ID-settet i én spørring, som i teamside-eksempelet. For svært brede profiler er en denormalisert katalograd skrevet ved profil-lagring enda billigere.
Hvem kan liste brukere via WordPress REST API
Malen din kan være låst mens /wp-json/wp/v2/users fortsatt svarer. Hva den returnerer avhenger av den besøkende, og reglene står i WP_REST_Users_Controller, oppsummert i referansen for users-endepunktet:
- En besøkende uten kapasiteten
list_users(som standard har bare administratorer den) ser bare brukere som har publisert innhold i en innholdstype eksponert for REST API. Medlemmer som aldri har publisert, er ikke med i den anonyme listen. - I standardkonteksten
viewhar et brukerobjektid,name,url,description,link,slugogavatar_urls.username,email,rolesogregistered_datefinnes bare i kontekstenedit, som kreverlist_users. - Filtrering på
rolesellercapabilities, sortering påemailellerregistered_dateogcontext=editgir autorisasjonsfeil utenlist_users. - For disse besøkende er et søk låst til
ID,user_login,user_nicenameogdisplay_name. Parameterensearch_columns, lagt til i WordPress 6.8, kan ikke utvide listen. per_pageer 10 som standard og maks 100, og svaret har headerneX-WP-TotalogX-WP-TotalPages.
Feltet slug er svakheten. wp_insert_user() bygger user_nicename fra login når ingen nicename er oppgitt, ved å kjøre de første 50 tegnene av user_login gjennom sanitize_title(). For de fleste kontoer er den offentlige sluggen derfor login, eller nesten. Samme nicename står i URL-ene til forfatterarkivene.
To filtre gir deg kontroll. rest_user_query får WP_User_Query-argumentene for hver listeforespørsel, så du kan legge til en synlighets-meta_query der. Har nettstedet ingen offentlig bruk for endepunktet, fjern det bare for anonyme besøkende, fordi blokkredigereren leser brukere via samme rute for innloggede redaktører:
add_filter(
'rest_endpoints',
static function ( $endpoints ) {
if ( ! is_user_logged_in() ) {
unset( $endpoints['/wp/v2/users'], $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
}
return $endpoints;
}
);Slik hindrer du user enumeration i WordPress
En medlemskatalog er en offentlig kontoliste. Behandle den deretter. Botene som treffer /?author=1 bryr seg ikke om at katalogen din «bare er for medlemmer» hvis HTML-en allerede lekker login-navn.
- Aldri
echo $user->user_logini frontend. Brukdisplay_nameeller et offentlig kallenavn.user_nicenameer avledet av login som standard og er derfor ingen trygg erstatning. - Aldri
user_emailuten innlogging og kapasitet. - Forfatterarkiv (
/?author=1) lekker ofte login. Trenger du dem ikke:
add_action(
'template_redirect',
static function () {
if ( is_author() ) {
wp_safe_redirect( home_url( '/' ), 301 );
exit;
}
}
);- Escape alt:
esc_html(),esc_url(),esc_attr(). - Admin-kataloger bak
list_users, ikke bak «hemmelig» URL. - Offentlig søk med
search_columnsbegrenset tildisplay_name, som vist over.
GDPR og medlemskatalog: hvem skal være synlig
For medlemmer i EU/EØS er en katalog over personer personopplysninger. Navn, bilder, by og ferdigheter for identifiserbare personer er personopplysninger etter artikkel 4 nr. 1 i personvernforordningen (GDPR). Artikkel 25 nr. 2 legger til en regel som passer rett på spørringsargumentene dine: som standard skal personopplysninger ikke gjøres tilgjengelige for et ubegrenset antall personer uten at den registrerte selv griper inn.
Praktisk i kode:
- Standard for nye kontoer:
is_public_profile=0til brukeren aktivt krysser av. - Egne felt for «vis telefon» / «vis e-post» - ikke anta at profil = offentlig kontaktinfo.
- Hver offentlig spørring har synlighetsklausulen i sin
meta_query, og det samme gjelder et eventueltrest_user_query-filter. - Staging-databaser med ekte medlemmer skal anonymiseres før de deles med noen utenfor teamet.
WP_User_Query filtrerer teknisk. Den erstatter ikke samtykke, formål eller slettepolicy. Men uten synlighetsflagg i meta_query lekker du profiler uansett hvor fin personvernerklæringen er.
Medlemsplugin eller egen katalog med WP_User_Query
Tunge medlemsplugins løser påmelding, betalingsnivåer og meldinger. Den offentlige listen er et mindre problem, og det er den som kjører på hver anonyme forespørsel. Behold plugin for kontoflyt. Bygg listen selv med WP_User_Query når den må være rask eller sterkt filtrert.
Er du allerede låst til pluginens grid, mål den før du godtar «det er greit nok»: åpne listen med Query Monitor og sjekk at brukerspørringen har LIMIT, bare henter feltene kortene viser, og ikke utløser én meta-spørring per kort.
Mal for medlemskatalog i WordPress
En offentlig katalog som tåler drift har vanligvis fire lag:
- Input - allow-list for sortering, heltall for side, normalisert by-slug (
oslo, ikke fri tekst rett i SQL). - Spørring -
role__in/meta_query/fields/number/paged. - Presentasjon - kort fra ID-er og trygge visningsfelt.
- Cache-invalidering - knyttet til meta-nøklene som styrer synlighet.
La plugin håndtere påmelding og meldinger hvis du trenger det. La listing og filtrering ligge i temaet eller en liten mu-plugin du eier. Det skillet holder katalogen rask når markedet ber om «ett filter til» neste kvartal.
WP_User_Query på multisite med roller per nettsted
På multisite er brukere delt, roller er per site. En redaktør på site A trenger ikke ha noen rolle på site B. For nettverksomfattende ansatte-lister: nettverksmeta (wpp_show_in_directory) i stedet for bare role__in. Sett blog_id når roller skal scopes til ett nettsted.
Kildekoden forklarer hvorfor. På multisite legger selv en spørring uten rolleargumenter til en EXISTS-klausul på nøkkelen {prefix}capabilities for gjeldende nettsted, så bare medlemmer av det nettstedet kommer tilbake. Med 'blog_id' => 0 hoppes rolle- og medlemskapsklausulene over, og spørringen går mot hele nettverket.
Staging-kopier av multisite kutter ofte brukertabellen. Mål spørringskostnad på et dump som matcher produksjonsvolum - ellers ser meta_query-planen fin ut til lanseringsdagen.
Slik tester du WP_User_Query-SQL med Query Monitor
- Query Monitor på staging.
- Åpne katalog-URL med tyngste filterkombinasjon markedet bruker (by + fag + språk er en klassiker).
- Bekreft én primær brukerspørring, ikke én meta-spørring per kort.
- Lagre
$query->requesti PR-notater så neste person ser regresjoner.
Fritekst-søk på tvers av navn: dedikert søkeindeks eller stramt search-arg på WP_User_Query. Ikke LIKE mot user_email på offentlig endepunkt. Logg trege katalog-forespørsler med filter-payload så du kan gjenskape dem.
Sjekkliste for medlemskatalog med WP_User_Query
fieldssatt - ikke fulle objekter til kort-grid, og maks tre felt for å beholde den innebygde cachen.- Paginering med
number+pagedogget_total();count_total => falsenår det ikke pagineres. - Eksakte meta-nøkler fremfor
LIKEpå serialiserte arrays. - Meta forhåndslastet med
update_meta_cache()før løkken. - Cache av ID-lister der filtrene er stabile; invalider ved profilskriving.
- Bare
display_namei offentlig HTML og offentlig søk; login, nicename og e-post holdes unna. - Synlighetsflagg i
meta_queryfor alle offentlige lister, også via/wp/v2/users. - Mål
$query->requestmed realistisk bruker- og metavolum. - Forfatterarkiv redirectet hvis produktet ikke trenger dem.
Brukere er førstklassige spørringsmål i WordPress. Behandle dem med samme disiplin som WP_Query.
Trenger du en skreddersydd medlemskatalog eller ytelsesgjennomgang av brukerspørringer, ta kontakt - vi bygger og drifter slike løsninger i WordPress.
På leveransesiden holder vi dette temaet under Core Web Vitals-revisjon.






