Come filtrare gli utenti nella pagina admin degli utenti tramite un campo meta personalizzato?
Il Problema
WordPress sembra rimuovere il valore della mia variabile di query prima che venga utilizzata per filtrare l'elenco degli utenti.
Il Mio Codice
Questa funzione aggiunge una colonna personalizzata alla mia tabella Utenti su /wp-admin/users.php
:
function add_course_section_to_user_meta( $columns ) {
$columns['course_section'] = 'Sezione';
return $columns;
}
add_filter( 'manage_users_columns', 'add_course_section_to_user_meta' );
Questa funzione dice a WordPress come riempire i valori nella colonna:
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' );
Questo aggiunge un menu a tendina e un pulsante Filtra
sopra la tabella Utenti:
function add_course_section_filter() {
echo '<select name="course_section" style="float:none;">';
echo '<option value="">Sezione Corso...</option>';
for ( $i = 1; $i <= 3; ++$i ) {
if ( $i == $_GET[ 'course_section' ] ) {
echo '<option value="'.$i.'" selected="selected">Sezione '.$i.'</option>';
} else {
echo '<option value="'.$i.'">Sezione '.$i.'</option>';
}
}
echo '<input id="post-query-submit" type="submit" class="button" value="Filtra" name="">';
}
add_action( 'restrict_manage_users', 'add_course_section_filter' );
Questa funzione modifica la query utente per aggiungere la mia 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' );
Altre Informazioni
Crea correttamente il mio menu a tendina. Quando seleziono una sezione del corso e faccio clic su Filtra
la pagina si aggiorna e course_section
appare nell'URL, ma non ha alcun valore associato. Se controllo le richieste HTTP, mostra che viene inviato con il valore corretto della variabile, ma poi c'è un 302 Redirect
che sembra eliminare il valore che ho selezionato.
Se invio la variabile course_section
digitandola direttamente nell'URL, il filtro funziona come previsto.
Il mio codice è approssimativamente basato su questo codice di Dave Court.
Ho anche provato a inserire nella whitelist la mia variabile di query usando questo codice, ma senza successo:
function add_course_section_query_var( $qvars ) {
$qvars[] = 'course_section';
return $qvars;
}
add_filter( 'query_vars', 'add_course_section_query_var' );
Sto usando WP 4.4. Qualche idea sul perché il mio filtro non funziona?

AGGIORNAMENTO 2018-06-28
Sebbene il codice qui sotto funzioni per lo più correttamente, ecco una riscrittura del codice per WP >=4.6.0 (utilizzando PHP 7):
function add_course_section_filter( $which ) {
// crea template sprintf per <select> e <option>
$st = '<select name="course_section_%s" style="float:none;"><option value="">%s</option>%s</select>';
$ot = '<option value="%s" %s>Sezione %s</option>';
// determina quale pulsante filtro è stato cliccato, se presente, e imposta la sezione
$button = key( array_filter( $_GET, function($v) { return __( 'Filtra' ) === $v; } ) );
$section = $_GET[ 'course_section_' . $button ] ?? -1;
// genera codice <option> e <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, __( 'Sezione Corso...' ), $options );
// output <select> e pulsante submit
echo $select;
submit_button(__( 'Filtra' ), 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 __( 'Filtra' ) === $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');
Ho incorporato diverse idee da @birgire e @cale_b che offrono anche soluzioni qui sotto che vale la pena leggere. Nello specifico, ho:
- Utilizzato la variabile
$which
aggiunta inv4.6.0
- Utilizzato le best practice per l'i18n usando stringhe traducibili, es.
__( 'Filtra' )
- Sostituito i loop con (più alla moda?)
array_map()
,array_filter()
, erange()
- Utilizzato
sprintf()
per generare i template di markup - Utilizzato la notazione array con parentesi quadre invece di
array()
Infine, ho scoperto un bug nelle mie soluzioni precedenti. Quelle soluzioni favorivano sempre il <select>
SUPERIORE rispetto a quello INFERIORE. Quindi se selezionavi un'opzione di filtro dal menu a discesa superiore, e successivamente ne selezionavi una dal menu a discesa inferiore, il filtro utilizzava comunque solo il valore presente in alto (se non era vuoto). Questa nuova versione corregge quel bug.
AGGIORNAMENTO 2018-02-14
Questo problema è stato risolto a partire da WP 4.6.0 e le modifiche sono documentate nella documentazione ufficiale. La soluzione qui sotto funziona comunque.
Cosa Causava il Problema (WP <4.6.0)
Il problema era che l'azione restrict_manage_users
viene chiamata due volte: una sopra la tabella degli utenti e una sotto. Questo significa che vengono creati DUE menu a discesa select
con lo stesso nome. Quando si clicca il pulsante Filtra
, qualsiasi valore sia presente nel secondo elemento select
(cioè quello SOTTO la tabella) sovrascrive il valore nel primo, cioè quello SOPRA la tabella.
Se vuoi approfondire il codice sorgente di WP, l'azione restrict_manage_users
viene attivata all'interno di WP_Users_List_Table::extra_tablenav($which)
, che è la funzione che crea il menu a discesa nativo per cambiare il ruolo di un utente. Quella funzione ha l'aiuto della variabile $which
che le dice se sta creando il select
sopra o sotto il form, e le permette di dare ai due menu a discesa attributi name
diversi. Purtroppo, la variabile $which
non viene passata all'azione restrict_manage_users
, quindi dobbiamo trovare un altro modo per differenziare i nostri elementi personalizzati.
Un modo per farlo, come suggerisce @Linnea, sarebbe aggiungere del JavaScript per catturare il click su Filtra
e sincronizzare i valori dei due menu a discesa. Io ho scelto una soluzione solo PHP che descriverò ora.
Come Risolverlo
Puoi sfruttare la possibilità di trasformare gli input HTML in array di valori, e poi filtrare l'array per eliminare i valori non definiti. Ecco il codice:
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="">Sezione Corso...</option>';
for ( $i = 1; $i <= 3; ++$i ) {
$selected = $i == $section ? ' selected="selected"' : '';
echo '<option value="' . $i . '"' . $selected . '>Sezione ' . $i . '</option>';
}
echo '</select>';
echo '<input type="submit" class="button" value="Filtra">';
}
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: Refactoring in PHP 7
Dato che sono entusiasta di PHP 7, nel caso tu stia eseguendo WP su un server PHP 7, ecco una versione più breve e più elegante che utilizza l'operatore di coalescenza null ??
:
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="">Sezione Corso...</option>';
for ( $i = 1; $i <= 3; ++$i ) {
$selected = $i == $section ? ' selected="selected"' : '';
echo '<option value="' . $i . '"' . $selected . '>Sezione ' . $i . '</option>';
}
echo '</select>';
echo '<input type="submit" class="button" value="Filtra">';
}
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' );
Buon divertimento!

Quindi la tua soluzione funziona ancora dopo la versione 4.6.0? C'è un modo più semplice per farlo con l'ultima versione di WordPress? Non riesco a trovare guide fatte, diciamo, quest'anno

@JeremyMuckel la risposta breve alla tua domanda è "sì". La mia vecchia soluzione funziona ancora. L'ho usata regolarmente in produzione per mesi e la maggior parte dei miei siti è aggiornata all'ultima versione stabile di WP (attualmente 4.9.6). Detto questo, ho fornito una soluzione aggiornata che utilizza la nuova patch e che corregge anche un bug sottile nella mia soluzione precedente.

Questo è stato utile ma il tuo codice del form sotto "Come Risolverlo" e "Bonus: Refactoring per PHP 7" manca di un </select>
. Ho anche scoperto che per farlo funzionare devo inserire <form method="get">
prima del menu a selezione e un </form>
dopo il pulsante del filtro.

@cogdog ottimo occhio per i tag </select>
mancanti! Li ho aggiunti. Strano che tu abbia avuto bisogno di incapsularlo in un <form>
visto che l'intera pagina è già racchiusa in un grande form, e questo codice viene iniettato nel mezzo. Comunque sono contento che tu l'abbia fatto funzionare. :)

Ho testato il tuo codice sia in WordPress 4.4 che in WordPress 4.3.1. Con la versione 4.4, ho riscontrato esattamente lo stesso problema che hai descritto. Tuttavia, il tuo codice funziona correttamente nella versione 4.3.1!
Penso che si tratti di un bug di WordPress. Non so se sia già stato segnalato. Credo che la ragione dietro questo bug potrebbe essere che il pulsante di invio sta inviando i query vars due volte. Se osservi i query vars, vedrai che course_section è elencato due volte, una con il valore corretto e una vuoto.
Modifica: Questa è la Soluzione JavaScript
Basta aggiungere questo al file functions.php del tuo tema e cambiare NAME_OF_YOUR_INPUT_FIELD con il nome del tuo campo di input! Dato che WordPress carica automaticamente jQuery nella parte di amministrazione, non devi caricare alcuno script. Questo frammento di codice aggiunge semplicemente un listener di cambio agli input a discesa e poi aggiorna automaticamente l'altro menu a discesa per corrispondere allo stesso valore. Maggiori spiegazioni qui.
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
} );
Spero che questo aiuti!

Grazie Linnea. Sì, ho riscontrato la stessa cosa, quando clicchi su Filtra
invia il valore corretto, ma poi reindirizza nuovamente alla pagina, questa volta rimuovendo il valore. Immagino sia una sorta di "funzionalità" di sicurezza per prevenire l'invio di valori casuali potenzialmente malevoli, ma non so come aggirarlo. Uffa.

OH! Ho capito perché la variabile appare due volte. Perché c'è un menu a discesa sia SOPRA che SOTTO la tabella degli utenti e entrambi hanno lo stesso attributo name
. Se uso il menu a discesa SOTTO la tabella per filtrare, funziona come previsto. Poiché quel campo viene dopo quello sopra, il suo valore nullo sovrascrive il precedente. Mmm...

Nel core, i nomi degli input in basso sono contrassegnati con il numero dell'istanza, ad esempio new_role
(in alto) e new_role2
(in basso). Ecco due approcci per una convenzione di denominazione simile, ovvero course_section1
(in alto) e course_section2
(in basso):
Approccio #1
Poiché la variabile $which
(top, bottom) non viene passata all'hook restrict_manage_users
, potremmo aggirare il problema creando la nostra versione di quell'hook:
Creiamo l'action hook wpse_restrict_manage_users
che ha accesso a una variabile $which
:
add_action( 'restrict_manage_users', function()
{
static $instance = 0;
do_action( 'wpse_restrict_manage_users', 1 === ++$instance ? 'top' : 'bottom' );
} );
Quindi possiamo agganciarlo con:
add_action( 'wpse_restrict_manage_users', function( $which )
{
$name = 'top' === $which ? 'course_section1' : 'course_section2';
// il tuo codice qui
} );
dove ora abbiamo $name
come course_section1
in alto e course_section2
in basso.
Approccio #2
Agganciamoci a restrict_manage_users
per visualizzare i dropdown, con un nome diverso per ogni istanza:
function add_course_section_filter()
{
static $instance= 0;
// Opzioni del dropdown
$options = '';
foreach( range( 1, 3 ) as $rng )
{
$options = sprintf(
'<option value="%1$d" %2$s>Sezione %1$d</option>',
$rng,
selected( $rng, get_selected_course_section(), 0 )
);
}
// Visualizza dropdown con un nome diverso per ogni istanza
printf(
'<select name="%s" style="float:none;"><option value="0">%s</option>%s</select>',
'course_section' . ++$instance,
__( 'Sezione del corso...' ),
$options
);
// Pulsante
printf (
'<input id="post-query-submit" type="submit" class="button" value="%s" name="">',
__( 'Filtra' )
);
}
add_action( 'restrict_manage_users', 'add_course_section_filter' );
dove abbiamo utilizzato la funzione core selected()
e la funzione helper:
/**
* Ottiene la sezione del corso selezionata
* @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; // valore predefinito
return (int) $course_section;
}
Potremmo anche utilizzare questo quando controlliamo la sezione del corso selezionata nella callback dell'azione pre_get_users
.

Questo è un approccio affascinante. Non ho mai usato la parola chiave static
in questo modo (solo all'interno delle classi). $instance
diventa una variabile globale quando si fa così? Bisogna preoccuparsi di collisioni nei nomi delle variabili? Mi piace anche la tecnica di creare una nuova azione che sfrutta una già esistente. Grazie!

Questo approccio può essere utile a volte ed è usato nel core per esempio per contare le istanze degli shortcode (gallery, playlist, audio). L'ambito della variabile statica qui non interferirà con l'ambito delle variabili globali. Il valore della variabile statica verrà preservato tra le chiamate di funzione, cosa che non accade con le variabili locali. Ho cercato e trovato questo bel tutorial che ha maggiori dettagli. @morphatic

Questa è una diversa soluzione Javascript che potrebbe essere utile per alcune persone. Nel mio caso ho semplicemente rimosso completamente il secondo menu a tendina (in basso). Ho notato che tanto non uso mai gli input in basso...
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
} );

Soluzione senza JavaScript
Assegna al select un nome in stile "array", in questo modo:
echo '<select name="course_section[]" style="float:none;">';
In questo modo verranno passati ENTRAMBI i parametri (dall'alto e dal fondo della tabella), ora in un formato di array riconosciuto.
Il valore può quindi essere utilizzato così nella funzione pre_get_users
:
function filter_users_by_course_section( $query ) {
global $pagenow;
// se non siamo nella pagina utenti in admin, usciamo
if ( ! is_admin() || 'users.php' != $pagenow ) {
return;
}
// se nessuna sezione è selezionata, usciamo
if ( empty( $_GET['course_section'] ) ) {
return;
}
// course_section è ora sicuramente impostato, quindi lo carichiamo
$section = $_GET['course_section'];
// il valore è un array, e uno dei due select box probabilmente
// non era impostato, quindi usiamo array_filter per eliminare elementi vuoti
$section = array_filter( $section );
// il valore è ancora un array, quindi prendiamo il primo valore
$section = reset( $section );
// ora il valore è un singolo valore, ad esempio 1
$meta_query = array(
array(
'key' => 'course_section',
'value' => $section
)
);
$query->set( 'meta_key', 'course_section' );
$query->set( 'meta_query', $meta_query );
}

un'altra soluzione
puoi inserire la tua casella di selezione del filtro in un file separato come user_list_filter.php
e usare require_once 'user_list_filter.php'
nella tua funzione di callback dell'azione
file user_list_filter.php
:
<select name="course_section" style="float:none;">
<option value="">Sezione Corso...</option>
<?php for ( $i = 1; $i <= 3; ++$i ) {
if ( $i == $_GET[ 'course_section' ] ) { ?>
<option value="<?=$i?>" selected="selected">Sezione <?=$i?></option>
<?php } else { ?>
<option value="<?=$i?>">Sezione <?=$i?></option>
<?php }
}?>
</select>
<input id="post-query-submit" type="submit" class="button" value="Filtra" name="">
e nella tua callback dell'azione:
function add_course_section_filter() {
require_once 'user_list_filter.php';
}

Ho preso la soluzione di morphatic e l'ho resa un po' più dinamica:
// Aggiunge un filtro personalizzato alla tabella degli utenti nel pannello di amministrazione
function add_custom_users_table_filter($id, $placeholder_text, $filter_options, $query_modifier, $button_text = 'Filtra') {
$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]);
}
e poi puoi usarla così:
add_action('load-users.php', 'init_custom_users_table_functions');
function init_custom_users_table_functions() {
// Aggiunge il filtro "Sezione Corso" alla tabella degli utenti
add_custom_users_table_filter(
'course_section',
'Sezione Corso...',
array(
'1' => 'Sezione 1',
'2' => 'Sezione 2',
'3' => 'Sezione 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;
}
);
}
La cosa migliore è che la funzione può essere chiamata più volte per aggiungere più filtri.
