Cum să filtrezi utilizatorii în pagina de administrare după un câmp meta personalizat?
Problema
WordPress pare să elimine valoarea variabilei query înainte să fie folosită pentru filtrarea listei de utilizatori.
Codul Meu
Această funcție adaugă o coloană personalizată în tabelul Utilizatori din /wp-admin/users.php
:
function add_course_section_to_user_meta( $columns ) {
$columns['course_section'] = 'Secțiune';
return $columns;
}
add_filter( 'manage_users_columns', 'add_course_section_to_user_meta' );
Această funcție spune WordPress-ului cum să completeze valorile în coloană:
function manage_users_course_section( $val, $col, $uid ) {
if ( 'course_section' === $col )
return get_the_author_meta( 'course_section', $uid );
}
add_filter( 'manage_users_custom_column', 'manage_users_course_section' );
Aceasta adaugă un dropdown și butonul Filtrare
deasupra tabelului Utilizatori:
function add_course_section_filter() {
echo '<select name="course_section" style="float:none;">';
echo '<option value="">Secțiune Curs...</option>';
for ( $i = 1; $i <= 3; ++$i ) {
if ( $i == $_GET[ 'course_section' ] ) {
echo '<option value="'.$i.'" selected="selected">Secțiunea '.$i.'</option>';
} else {
echo '<option value="'.$i.'">Secțiunea '.$i.'</option>';
}
}
echo '<input id="post-query-submit" type="submit" class="button" value="Filtrare" name="">';
}
add_action( 'restrict_manage_users', 'add_course_section_filter' );
Această funcție modifică interogarea utilizatorilor pentru a adăuga meta_query
:
function filter_users_by_course_section( $query ) {
global $pagenow;
if ( is_admin() &&
'users.php' == $pagenow &&
isset( $_GET[ 'course_section' ] ) &&
!empty( $_GET[ 'course_section' ] )
) {
$section = $_GET[ 'course_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' );
Alte Informații
Creează dropdown-ul corect. Când selectez o secțiune de curs și dau click pe Filtrare
pagina se reîncarcă și course_section
apare în URL, dar nu are nicio valoare asociată. Dacă verific cererile HTTP, se arată că este trimis cu valoarea variabilei corectă, dar apoi există o 302 Redirect
care pare să elimine valoarea pe care am selectat-o.
Dacă trimit variabila course_section
scriind-o direct în URL, filtrul funcționează așa cum era de așteptat.
Codul meu se bazează aproximativ pe acest cod de la Dave Court.
Am încercat de asemenea să adaug variabila query în lista albă folosind acest cod, dar fără succes:
function add_course_section_query_var( $qvars ) {
$qvars[] = 'course_section';
return $qvars;
}
add_filter( 'query_vars', 'add_course_section_query_var' );
Folosesc WordPress 4.4. Aveți idei de ce nu funcționează filtrul meu?

ACTUALIZARE 2018-06-28
Deși codul de mai jos funcționează în mare parte bine, iată o rescriere a codului pentru WP >=4.6.0 (folosind PHP 7):
function add_course_section_filter( $which ) {
// crește șabloane sprintf pentru <select> și <option>s
$st = '<select name="course_section_%s" style="float:none;"><option value="">%s</option>%s</select>';
$ot = '<option value="%s" %s>Secțiunea %s</option>';
// determină ce buton de filtrare a fost apăsat, dacă există și setează secțiunea
$button = key( array_filter( $_GET, function($v) { return __( 'Filtrează' ) === $v; } ) );
$section = $_GET[ 'course_section_' . $button ] ?? -1;
// generează codul pentru <option> și <select>
$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, __( 'Secțiunea cursului...' ), $options );
// afișează <select> și butonul de trimitere
echo $select;
submit_button(__( 'Filtrează' ), 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 __( 'Filtrează' ) === $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');
Am încorporat câteva idei de la @birgire și @cale_b care oferă, de asemenea, soluții mai jos care merită citite. Mai exact, am:
- Folosit variabila
$which
care a fost adăugată înv4.6.0
- Folosit cele mai bune practici pentru i18n prin utilizarea șirurilor traductibile, de ex.
__( 'Filtrează' )
- Înlocuit buclele cu (mai la modă?)
array_map()
,array_filter()
șirange()
- Folosit
sprintf()
pentru generarea șabloanelor de markup - Folosit notația de matrice cu paranteze pătrate în loc de
array()
În cele din urmă, am descoperit un bug în soluțiile mele anterioare. Acele soluții întotdeauna favorizează <select>
-ul de SUS față de <select>
-ul de JOS. Deci, dacă selectați o opțiune de filtrare din meniul derulant de sus și apoi selectați una din cel de jos, filtrul va folosi doar valoarea care era sus (dacă nu este goală). Această nouă versiune corectează acest bug.
ACTUALIZARE 2018-02-14
Această problemă a fost rezolvată începând cu WP 4.6.0 și modificările sunt documentate în documentația oficială. Soluția de mai jos funcționează totuși.
Ce a Cauzat Problema (WP <4.6.0)
Problema a fost că acțiunea restrict_manage_users
este apelată de două ori: o dată DEASUPRA tabelului Utilizatori și o dată DEDESUBTUL acestuia. Acest lucru înseamnă că două meniuri derulante select
sunt create cu același nume. Când butonul Filtrează
este apăsat, orice valoare se află în al doilea element select
(adică cel DEDESUBTUL tabelului) înlocuiește valoarea din primul, adică cel DEASUPRA tabelului.
În cazul în care doriți să vă scufundați în sursa WP, acțiunea restrict_manage_users
este declanșată din WP_Users_List_Table::extra_tablenav($which)
, care este funcția care creează meniul derulant nativ pentru a schimba rolul unui utilizator. Acea funcție are ajutorul variabilei $which
care îi spune dacă creează select
-ul deasupra sau dedesubtul formularului și îi permite să dea celor două meniuri derulante atribute name
diferite. Din păcate, variabila $which
nu este transmisă acțiunii restrict_manage_users
, așa că trebuie să găsim o altă modalitate de a diferenția elementele noastre personalizate.
O modalitate de a face acest lucru, așa cum sugerează @Linnea, ar fi să adăugăm niște JavaScript pentru a prinde clicul pe Filtrează
și a sincroniza valorile celor două meniuri derulante. Am ales o soluție doar în PHP pe care o voi descrie acum.
Cum să o Reparați
Puteți profita de capacitatea de a transforma input-urile HTML în matrice de valori și apoi să filtrați matricea pentru a elimina orice valori nedefinite. Iată codul:
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="">Secțiunea cursului...</option>';
for ( $i = 1; $i <= 3; ++$i ) {
$selected = $i == $section ? ' selected="selected"' : '';
echo '<option value="' . $i . '"' . $selected . '>Secțiunea ' . $i . '</option>';
}
echo '</select>';
echo '<input type="submit" class="button" value="Filtrează">';
}
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: Refactorizare PHP 7
Deoarece sunt entuziasmat de PHP 7, în cazul în care rulați WP pe un server PHP 7, iată o versiune mai scurtă, mai atrăgătoare folosind operatorul de coalescență nulă ??
:
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="">Secțiunea cursului...</option>';
for ( $i = 1; $i <= 3; ++$i ) {
$selected = $i == $section ? ' selected="selected"' : '';
echo '<option value="' . $i . '"' . $selected . '>Secțiunea ' . $i . '</option>';
}
echo '</select>';
echo '<input type="submit" class="button" value="Filtrează">';
}
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' );
Distracție!

Deci soluția ta încă funcționează după versiunea 4.6.0? Există o cale mai ușoară de a face asta cu cea mai nouă versiune de WordPress? Nu reușesc să găsesc niciun ghid făcut în acest an.

@JeremyMuckel răspunsul scurt la întrebarea ta este "da". Vechea mea soluție încă funcționează. Am folosit-o în producție în mod regulat de luni de zile și majoritatea site-urilor mele sunt actualizate la cea mai recentă versiune stabilă de WP (în prezent 4.9.6). Cu toate acestea, am oferit o soluție actualizată care folosește noul patch și care rezolvă și o mică eroare din soluția mea anterioară.

Aceasta a fost utilă, dar codul tău de formular din secțiunea "How to Fix It" și "Bonus: PHP 7 Refactor" lipsește un </select>
. De asemenea, am constatat că pentru a funcționa a trebuit să pun <form method="get">
înaintea meniului select și un </form>
după butonul de filtrare.

@cogdog bună observație pentru tag-urile lipsă </select>
! Le-am adăugat. Ciudat că a trebuit să le înfășori într-un <form>
deoarece întreaga pagină este deja înfășurată într-un formular mare, iar acest cod este injectat în mijlocul lui. Mă bucur că ai reușit să-l faci să funcționeze, totuși. :)

Am testat codul tău atât în WordPress 4.4 cât și în WordPress 4.3.1. Cu versiunea 4.4, întâmpin exact aceeași problemă ca și tine. Totuși, codul tău funcționează corect în versiunea 4.3.1!
Cred că aceasta este o eroare în WordPress. Nu știu dacă a fost raportată încă. Cred că motivul din spatele acestei erori ar putea fi că butonul de trimitere trimite variabilele de interogare de două ori. Dacă te uiți la variabilele de interogare, vei vedea că course_section este listat de două ori, o dată cu valoarea corectă și o dată gol.
Edit: Aceasta este soluția JavaScript
Pur și simplu adaugă acest cod în fișierul functions.php al temei tale și schimbă NAME_OF_YOUR_INPUT_FIELD cu numele câmpului tău de introducere! Deoarece WordPress încarcă automat jQuery în partea de administrare, nu trebuie să încarci scripturi suplimentare. Acest fragment de cod adaugă pur și simplu un ascultător de schimbare la meniurile derulante și apoi actualizează automat celălalt meniu derulant pentru a avea aceeași valoare. Mai multe explicații aici.
add_action( 'in_admin_footer', function() {
?>
<script type="text/javascript">
var el = jQuery("[name='NAME_OF_YOUR_INPUT_FIELD']");
el.change(function() {
el.val(jQuery(this).val());
});
</script>
<?php
} );
Sper că acest lucru te ajută!

Mulțumesc Linnea. Da, am observat același lucru, că atunci când apeși pe Filtru
trimite valoarea corectă, dar apoi redirecționează înapoi către pagină, de data aceasta eliminând valoarea. Cred că este un fel de "caracteristică" de securitate pentru a preveni trimiterea de valori aleatorii, potențial malicioase, dar nu știu cum să o ocolesc. Of.

OH! Am înțeles de ce variabila apare de două ori. Pentru că există o listă derulantă atât DEASUPRA cât și DEDESUBTUL tabelului de utilizatori și ambele au același atribut name
. Dacă folosesc lista derulantă DE DEDESUBTUL tabelului pentru filtrare, funcționează cum trebuie. Deoarece acel câmp vine după cel de deasupra, valoarea sa nulă suprascrie pe cea anterioară. Hmmm....

În nucleu, numele de intrare din jos sunt marcate cu numărul instanței, de exemplu new_role
(sus) și new_role2
(jos). Iată două abordări pentru o convenție de denumire similară, și anume course_section1
(sus) și course_section2
(jos):
Abordarea #1
Deoarece variabila $which
(sus, jos) nu este transmisă către hook-ul restrict_manage_users
, am putea ocoli acest lucru prin crearea propriei noastre versiuni a acelui hook:
Să creăm hook-ul de acțiune wpse_restrict_manage_users
care are acces la o variabilă $which
:
add_action( 'restrict_manage_users', function()
{
static $instance = 0;
do_action( 'wpse_restrict_manage_users', 1 === ++$instance ? 'top' : 'bottom' );
} );
Apoi putem să-l conectăm cu:
add_action( 'wpse_restrict_manage_users', function( $which )
{
$name = 'top' === $which ? 'course_section1' : 'course_section2';
// codul tău aici
} );
unde acum avem $name
ca course_section1
în sus și course_section2
în jos.
Abordarea #2
Să conectăm în restrict_manage_users
pentru a afișa meniuri derulante, cu un nume diferit pentru fiecare instanță:
function add_course_section_filter()
{
static $instance= 0;
// Opțiuni meniu derulant
$options = '';
foreach( range( 1, 3 ) as $rng )
{
$options = sprintf(
'<option value="%1$d" %2$s>Secțiunea %1$d</option>',
$rng,
selected( $rng, get_selected_course_section(), 0 )
);
}
// Afișează meniul derulant cu un nume diferit pentru fiecare instanță
printf(
'<select name="%s" style="float:none;"><option value="0">%s</option>%s</select>',
'course_section' . ++$instance,
__( 'Secțiunea Cursului...' ),
$options
);
// Buton
printf (
'<input id="post-query-submit" type="submit" class="button" value="%s" name="">',
__( 'Filtrează' )
);
}
add_action( 'restrict_manage_users', 'add_course_section_filter' );
unde am folosit funcția din nucleu selected()
și funcția ajutătoare:
/**
* Obține secțiunea cursului selectată
* @return int $course_section
*/
function get_selected_course_section()
{
foreach( range( 1, 2) as $rng )
$course_section = ! empty( $_GET[ 'course_section' . $rng ] )
? $_GET[ 'course_section' . $rng ]
: -1; // implicit
return (int) $course_section;
}
Apoi am putea folosi acest lucru și când verificăm secțiunea cursului selectată în callback-ul acțiunii pre_get_users
.

Este o abordare fascinantă. Nu am folosit niciodată cuvântul cheie static
în acest fel (doar în cadrul claselor). Devine $instance
o variabilă globală atunci când faci asta? Trebuie să-ți faci griji privind coliziunile de nume de variabile? De asemenea, îmi place tehnica de a crea o acțiune nouă care se atașează la una existentă. Mulțumesc!

Această abordare poate fi utilă uneori și este folosită în nucleu pentru a număra, de exemplu, instanțe de shortcode (galerie, playlist, audio). Scope-ul variabilei statice aici nu va interfera cu scope-ul variabilelor globale. Valoarea variabilei statice va fi păstrată între apelurile acelei funcții, ceea ce nu este cazul cu variabilele locale. Am căutat și am găsit acest tutorial util care oferă mai multe detalii. @morphatic

Acesta este o altă soluție Javascript care poate fi utilă pentru unii oameni. În cazul meu, am eliminat complet a doua listă de selecție (cea de jos). Am observat că nu folosesc niciodată câmpurile de jos...
add_action( 'in_admin_footer', function() {
?>
<script type="text/javascript">
jQuery(".tablenav.bottom select[name='course_section']").remove();
jQuery(".tablenav.bottom input#post-query-submit").remove();
</script>
<?php
} );

Soluție fără JavaScript
Dați elementului select un nume în stil "array", astfel:
echo '<select name="course_section[]" style="float:none;">';
Apoi AMBII parametri sunt transmiși (de la partea de sus și de jos a tabelului), și acum într-un format array cunoscut.
Apoi, valoarea poate fi folosită astfel în funcția pre_get_users
:
function filter_users_by_course_section( $query ) {
global $pagenow;
// dacă nu suntem pe pagina de utilizatori în admin, ieșim
if ( ! is_admin() || 'users.php' != $pagenow ) {
return;
}
// dacă nu a fost selectată nicio secțiune, ieșim
if ( empty( $_GET['course_section'] ) ) {
return;
}
// course_section este acum cunoscut a fi setat, așa că îl încărcăm
$section = $_GET['course_section'];
// valoarea este un array, și unul dintre cele două select box-uri probabil
// nu a fost setat la nimic, așa că folosim array_filter pentru a elimina elementele goale
$section = array_filter( $section );
// valoarea este încă un array, așa că luăm prima valoare
$section = reset( $section );
// acum valoarea este o singură valoare, cum ar fi 1
$meta_query = array(
array(
'key' => 'course_section',
'value' => $section
)
);
$query->set( 'meta_key', 'course_section' );
$query->set( 'meta_query', $meta_query );
}

o altă soluție
poți pune caseta de selecție a filtrului într-un fișier separat precum user_list_filter.php
și folosește require_once 'user_list_filter.php'
în funcția ta de callback pentru acțiune
fișierul user_list_filter.php
:
<select name="course_section" style="float:none;">
<option value="">Secțiune Curs...</option>
<?php for ( $i = 1; $i <= 3; ++$i ) {
if ( $i == $_GET[ 'course_section' ] ) { ?>
<option value="<?=$i?>" selected="selected">Secțiunea <?=$i?></option>
<?php } else { ?>
<option value="<?=$i?>">Secțiunea <?=$i?></option>
<?php }
}?>
</select>
<input id="post-query-submit" type="submit" class="button" value="Filtrează" name="">
și în callback-ul tău pentru acțiune:
function add_course_section_filter() {
require_once 'user_list_filter.php';
}

Am luat soluția lui morphatic și am făcut-o puțin mai dinamică:
// Adaugă o opțiune de filtrare personalizată în tabelul utilizatorilor din panou
function add_custom_users_table_filter($id, $placeholder_text, $filter_options, $query_modifier, $button_text = 'Filtrează') {
$html_function_name = "users_table_{$id}_filter";
$query_function_name = $html_function_name . '_query';
$GLOBALS[$html_function_name] = function($which) use($id, $button_text, $placeholder_text, $filter_options) {
$select_template = '<label class="screen-reader-text" for="%s_%s">%s</label>';
$select_template .= '<select name="%s_%s" id="%s_%s">';
$select_template .= '<option value="">%s</option>';
$select_template .= '%s</select>';
$option_template = '<option value="%s" %s>%s</option>';
$button = key(array_filter($_GET, function($v) use($button_text) {
return $button_text === $v;
}));
$selection = $_GET[$id . '_' . $button] ?? -1;
$options = implode('', array_map(function($value, $key) use($option_template, $selection) {
return sprintf($option_template, $key, selected($key, $selection, false), $value);
}, array_values($filter_options), array_keys($filter_options)));
$select = sprintf($select_template, $id, $which, $placeholder_text, $id, $which, $id, $which, $placeholder_text, $options);
echo '</div><div class="alignleft actions">';
echo $select;
submit_button($button_text, null, $which, false);
};
$GLOBALS[$query_function_name] = function($query) use($id, $button_text, $query_modifier) {
global $pagenow;
if (is_admin() && $pagenow == 'users.php') {
$button = key(array_filter($_GET, function($v) use($button_text) {
return $button_text === $v;
}));
if (isset($_GET[$id . '_' . $button]) && ($selection = $_GET[$id . '_' . $button])) {
return $query_modifier($query, $selection);
}
}
return $query;
};
add_action('restrict_manage_users', $GLOBALS[$html_function_name]);
add_filter('pre_get_users', $GLOBALS[$query_function_name]);
}
și apoi o poți folosi astfel:
add_action('load-users.php', 'init_custom_users_table_functions');
function init_custom_users_table_functions() {
// Adaugă filtrul "Secțiune Curs" în tabelul utilizatorilor
add_custom_users_table_filter(
'course_section',
'Secțiune Curs...',
array(
'1' => 'Secțiunea 1',
'2' => 'Secțiunea 2',
'3' => 'Secțiunea 3'
),
function($query, $selection) {
$meta_query = array(
array(
'key' => 'course_section',
'value' => $selection
)
);
if (isset($query->query_vars['meta_query']) && ($og_meta_query = $query->query_vars['meta_query'])) {
$meta_query = array('relation' => 'AND', $og_meta_query, $meta_query);
}
$query->set('meta_query', $meta_query);
return $query;
}
);
}
Cel mai bine este că funcția poate fi apelată de mai multe ori pentru a adăuga mai multe filtre.
