WordPress как на ладони
wordpress jino

Свои фильтры в таблицах записей, комментариев, пользователей, таксономий

Статья о том, как добавлять дополнительные фильтры для списков записей (таблица записей в админ-панели), для списка комментариев (таблица комментов) или для списка пользователей (таблица пользователей). В статье теория и примеры.

Вспомните таблицу постов в админке - над таблицей есть два выпадающих списка: по дате и по рубрикам. Речь идет о таких фильтрах. Ниже показано как добавить свои фильтры (выпадающие списки) и затем обработать запрос, урезав список записей до значений фильтра.

Возможности описанных ниже фильтров можно расширить с помощью дополнительных сортируемых колонок в таблице. Фильтрами можно «урезать» список записей, а дальше сортировать этот список по колонкам.

Все хуки для вставки HTML фильтров

Фильтры в таблицу списка записей можно добавить для любого типа записи, комментариев или пользователей. Все это делается через нужный хук. Логика везде одна: добавляем HTML код выпадающего списка - поле формы <select>, а затем обрабатываем запрос.

Теперь, посмотрим на все возможные хуки.

Записи (включая вложения)

фильтры в списке записей
// Добавляет html только вверх таблицы записей в блок стандартных фильтров.
do_action( 'restrict_manage_posts', $post_type, $which );

wp-admin/includes/class-wp-posts-list-table.php

// Добавляет html вверх или вниз таблицы записей, после кнопки сабмита стандартных фильтров.
do_action( 'manage_posts_extra_tablenav', $which );

wp-admin/includes/class-wp-posts-list-table.php

Комментарии

filtry-kommentariev
// Добавляет html в верх таблицы комментариев в блоке стандартных фильтров.
do_action( 'restrict_manage_comments' );

wp-admin/includes/class-wp-comments-list-table.php

// Добавляет HTML вверх и вниз таблицы комментариев, после кнопки сабмита фильтров.
do_action( 'manage_comments_nav', $comment_status );

wp-admin/includes/class-wp-comments-list-table.php

Пользователи

filtry-polzovatelj
// Добавляет html вверх и вниз таблицы пользователей, в блок стандартных фильтров.
do_action( 'restrict_manage_users', $which );

wp-admin/includes/class-wp-users-list-table.php

Таксономии

Для таксономий таких фильтров нет. Как добавить туда фильтры смотрите в примере ниже.

Параметр $which

Во всех фильтрах может быть top или bottom, что соответственно означает выводить указанный код вверху или внизу таблицы.

Примеры

Фильтр в таблице записей

Допустим, есть тип записи event и таксономия season. Нам нужно добавить фильтр, чтобы в таблице записей можно было оставлять только записи из указанного элемента таксономии season.

// фильтр - добавим выпадающий список
//do_action( 'restrict_manage_posts', $this->screen->post_type ); // $which == top
add_action( 'restrict_manage_posts', 'add_event_table_filters');
function add_event_table_filters( $post_type ){

	echo '
	<select name="sel_season">
		<option value="-1">- все сезоны -</option>
		<option value="203" '. selected(203, @ $_GET['sel_season'], 0) .'>2016-2017</option>
		<option value="83"'.   selected(83, @ $_GET['sel_season'], 0) .'>2015-2016</option>
		<option value="154"'.  selected(154, @ $_GET['sel_season'], 0) .'>2014-2015</option>
	</select>';
	// для динамического построения селекта, можно использовать wp_dropdown_categories()
}

// Фильтрация: обработка запроса
add_action( 'pre_get_posts', 'add_event_table_filters_handler' );
function add_event_table_filters_handler( $query ) {
	if( ! is_admin() ) return; // выходим если не админка

	// убедимся что мы на нужной странице админки
	$cs = get_current_screen();
	if( empty($cs->post_type) || $cs->post_type != 'event' || $cs->id != 'edit-event' ) return;

	// сезон
	if( @ $_GET['sel_season'] != -1 ){
		$selected_id = @ $_GET['sel_season'] ?: 20;
		$query->set( 'tax_query', array([ 'taxonomy'=>'season', 'terms'=>$selected_id ]) );
	}

	if( empty($_GET['orderby']) && @ $_GET['sel_season'] != -1 ){
		$query->set( 'orderby', 'menu_order date' );
	}
}

Получим рабочий фильтр:

Добавление фильтров в таблицу списка записей wordpress

Фильтр в таблице комментариев

Условия: для комментарие устанавливается статус, который записывается в метаполе комментария. Нам нужно в таблице комментариев сделать возможность фильтровать список по статусу. У нас есть 3 статуса: question (Вопрос), thanks (Спасибо), useful (Полезный).

Задача: вывести эти статусы в «селекте» в фильтрах таблицы комментариев и обработать запрос - если выбран какой-то статус, то изменить запрос, оставив в в таблице только комментарии с указанным статусом.

<?php
// Добавляем HTML фильтра
// do_action( 'restrict_manage_comments' );
add_action('restrict_manage_comments', 'add_comment_filter_select');
function add_comment_filter_select(){
	$cond = @ $_GET['condition'];
	?>
	<select name="condition" class="comm_condition" onchange="window.add_param_to_URL(this);">
		<option value="" <?php selected('', $cond) ?> >- Все статусы -</option>
		<option value="question" <?php selected('need_answer', $cond) ?> >Вопрос</option>
		<option value="thanks" <?php selected('thanks', $cond) ?> >Благодарность</option>
		<option value="useful" <?php selected('useful', $cond) ?> >Полезный</option>
	</select>

	<script>
	// добавляет параметр запроса в URL и редиректит при выборе селекта
	window.add_param_to_URL = function(el){
		var href = window.location.href, sep = /[?]/.test(href) ? "&" : "?", name = el.name.replace(/[^a-z_-]/i,'');
		window.location = (new RegExp(name+'=?')).test(href) ? href.replace( (new RegExp('([?&]'+name+'=?)[^&]*')), (el.value ? "$1"+ el.value : '') ) : (href + sep + name + "="+ el.value);
	}
	</script>
	<?php
}

// изменение запроса - вариант 1
add_action('parse_comment_query', 'change_comment_request2');
function change_comment_request2( $query ){
	if( empty($_GET['condition']) || ! is_admin() ) return; // не наш запрос

	// убедимся что мы на странице таблицы комментов
	$cs = get_current_screen();
	if( $cs->base != 'edit-comments' || $cs->id != 'edit-comments' ) return;

	// выходим, если это запрос на получение номера страницы get_page_of_comment()
	$qv = $query->query_vars;
	if( $qv['fields'] == 'ids' && $qv['count'] ) return;

	// изменяем запрос
	$query->query_vars['meta_query'] = array(
		array(
			'key'   => 'condition',
			'value' => sanitize_key($_GET['condition'])
		),
	);
}

В результате получим такой рабочий фильтр:

дополнительный фильтр в таблице комментариев

Javascript и атрибут onchange добавлен для удобности. Чтобы при выборе значение фильтр применялся сразу, а не после нажатия на кнопку фильтр.

Другой хук изменения запроса комментариев: comments_clauses

Он позволяет изменить сам SQL запрос. Этот вариант сложнее, но дает больше возможностей, иногда это может пригодиться:

// Изменение запроса - вариант 2
add_filter('comments_clauses', 'change_comment_request', 10, 2);
function change_comment_request( $clauses, $query ){
	if( empty($_GET['condition']) || ! is_admin() ) return $clauses; // не наш запрос

	// убедимся что мы на странице таблицы комментов
	$cs = get_current_screen();
	if( $cs->base != 'edit-comments' || $cs->id != 'edit-comments' ) return $clauses;

	// выходим, если это запрос на получение номера страницы get_page_of_comment()
	$qv = $query->query_vars;
	if( $qv['fields'] == 'ids' && $qv['count'] ) return $clauses;

	// изменяем запрос
	global $wpdb;

	$clauses['join']   = " LEFT JOIN $wpdb->commentmeta cm ON (cm.comment_id = $wpdb->comments.comment_ID)";
	$clauses['where'] .= $wpdb->prepare(" AND cm.meta_key='condition' AND cm.meta_value = %s", $_GET['condition'] );

	return $clauses;
}

Фильтр в таблице пользователей

Предположим, на сайте каждый пользователь должен быть активирован и при регистрации ему в метаполе activation_key записывается ключ активации. В момент активации это поле удаляется.

Задача: вывести в таблице пользователей фильтр, чтобы можно было показать только активированных пользователей или наоборот - только не активированных. Т.е. нам нужно вывести выпадающий список с выбором статуса пользователя (активирован, не активирован). И нужно обработать запрос, оставив в таблице пользователей в соответствии с запросом.

<?php
// Добавляет html вверх и вниз таблицы пользователей, в блок стандартных фильтров.
add_action( 'restrict_manage_users', 'add_users_list_filters' );
function add_users_list_filters( $which ){
	if( $which != 'top' ) return; // только наверху

	$activated = @ $_GET['u_activated'];

	?>
	<select name="u_activated" onchange="window.add_param_to_URL(this)">
		<option value="">Изменить статус...</option>
		<option value="yes" <?php selected('yes', $activated) ?> >Активированные</option>
		<option value="no" <?php selected('no', $activated) ?> >Не активированные</option>
	</select>

	<script>
	// добавляет параметр запроса в URL и редиректит при выборе селекта
	window.add_param_to_URL = function(el){
		var href = window.location.href, sep = /[?]/.test(href) ? "&" : "?", name = el.name.replace(/[^a-z_-]/i,'');
		window.location = (new RegExp(name+'=?')).test(href) ? href.replace( (new RegExp('([?&]'+name+'=?)[^&]*')), (el.value ? "$1"+ el.value : '') ) : (href + sep + name + "="+ el.value);
	}
	</script>
	<?php
}

// изменение запроса - вариант 1. С версии 4.0
add_action('pre_get_users', 'users_filter_handler');
function users_filter_handler( $uquery ){
	if( ! is_admin() || get_current_screen()->id !== 'users' ) return;

	global $wpdb;

	if( ! empty($_GET['u_activated']) ){
		$compare = $_GET['u_activated'] == 'yes' ? 'NOT EXISTS' : 'EXISTS';
		$uquery->set('meta_query', array( array('key'=>'activation_key', 'compare'=>$compare) ) );

		if( empty($_GET['orderby']) ){
			$uquery->set('order', 'DESC');
			$uquery->set('orderby', 'user_registered');
		}
	}
}

Другой хук изменения запроса пользователей: pre_user_query

// изменение запроса - вариант 2
add_action('pre_user_query', 'users_filter_handler2');
function users_filter_handler2( $uquery ){
	if( ! is_admin() || get_current_screen()->id !== 'users' ) return;

	global $wpdb;

	$vars = $uquery->query_vars;

	if( ! empty($_GET['u_activated']) && $activated = $_GET['u_activated'] ){
		$sql_sub = "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = 'activation_key'";
		if( $activated == 'yes' )
			$uquery->query_where .= " AND ID NOT IN ($sql_sub) ";
		else
			$uquery->query_where .= " AND ID IN ($sql_sub) ";

		$uquery->query_orderby = ' ORDER BY user_registered '. $vars['order'];
	}
}

Получим такой рабочий фильтр:

фильтр в таблице пользователей

Здесь также как и в прошлом примере. Я добавил яваскрипт, чтобы при изменении значения селекта оно сразу применялось и не надо было делать лишних движений - кликать на кнопку "фильтр".

Фильтр в таблице терминов (таксономий)

На момент версии WP 4.6, для таблицы элементов таксономии подобного фильтра не предусмотрено. Более того, я попытался найти обходные пути, в народе именуемые костылями, так тут тоже не разгуляться...

Ниже пожалуй, единственный способ добавить фильтры в таблицу элементов таксономий:

Это демонстрационный пример, где мы добавим возможность урезать список терминов в таблице до только родительских...

<?php
$taxonomy = 'category'; // такса ради которой все делается

// взываем ob_start()
add_action( "{$taxonomy}_add_form", function($taxonomy){
	ob_start();
} );

// вызываем ob_get_clean(), где получаем готовый HTML всей таблицы
// через preg_replace вставляем нужный нам HTML в нужно место
// Выводим что получилось на экран
add_action( "after-{$taxonomy}-table", function($taxonomy){
	$html = ob_get_clean();

	$__preg_replace_callback = function( $match ){
		$val = @ $_GET['parent_only'];
		ob_start();
		?>
		<div class="alignleft actions">
			<select name="parent_only" onchange="window.add_param_to_URL(this)">
				<option value="">Все уровни...</option>
				<option value="yes" <?php selected('yes', $val) ?> >Только родители</option>
			</select>
		</div>
		<script>
		// добавляет параметр запроса в URL и редиректит при выборе селекта
		window.add_param_to_URL = function(el){
			var href = window.location.href, sep = /[?]/.test(href) ? "&" : "?", name = el.name.replace(/[^a-z_-]/i,'');
			window.location = (new RegExp(name+'=?')).test(href) ? href.replace( (new RegExp('([?&]'+name+'=?)[^&]*')), (el.value ? "$1"+ el.value : '') ) : (href + sep + name + "="+ el.value);
		}
		</script>
		<?php
		return $match[1] . ob_get_clean();
	};

	echo preg_replace_callback('~(id="doaction[^<]+</div>)~', $__preg_replace_callback, $html );
} );
фильтр для списка терминов таксономий

Чтобы копать в этом направлении, нужно смотреть следующие файлы:

Хук изменения запроса

// Изменим запрос - вариант 1
// $this->query_vars = apply_filters( 'get_terms_args', $query, $taxonomies );
add_filter('get_terms_args', 'my_terms_filter_handler');
function my_terms_filter_handler( $query ){
	// проверяем, что мы там где нужно, а то небо упадет...
	if( empty($_GET['parent_only']) || ! is_admin() ) return $query;

	// убедимся что функция get_terms вызывается именно из класса с таблицей
	// она еще вызывается на этой же странице в выпадающем списке при создании термина...
	if( ! ( $query['fields'] == 'count' /*подсчета количества*/ || isset($query['page']) /*таблица терминов*/ ) )
		 return $query;

	/*
	// убедимся что функция get_terms вызывается именно из класса с таблицей
	$backtrace = debug_backtrace(false);
	$backtrace = array_pop( $backtrace );
	// для самой таблицы терминов и для подсчета количества
	if( in_array( @ $backtrace['class'], array('WP_List_Table','WP_Terms_List_Table') ) )
		echo $backtrace['class'];
	*/

	$query['parent'] = 0; // только родители
	return $query;
}

Другой хук для изменения SQL запроса: terms_clauses

Этот пример не относится к предыдущему коду. Это код сам по себе, который показывает как добавить параметры запроса по метаполям, чтобы отфильтровать термины.

Предполагается, что могут быть переданы 2 параметра фильтрации subject или grade. Каждый их них отвечает за свое метаполе у терминов. Параметры могут быть переданы вместе или по отдельности...

// изменяем SQL запрос
//add_filter('terms_clauses', 'subject_section_sortable_orderby', 10, 3 );
function subject_section_sortable_orderby( $pieces, $taxonomies, $args ){
	// хорошо проверяем, что мы там где нужно, а то ппц...
	if( (! @ $_GET['subject'] && ! @ $_GET['grade']) || ! is_admin() ) return $pieces;

	// убедимся что функция get_terms вызывается именно из класса с таблицей
	$backtrace = debug_backtrace(false);
	$backtrace = array_pop( $backtrace );
	if(
		( @ $backtrace['class'] != 'WP_List_Table') && // для самой таблицы терминов
		( @ $backtrace['class'] != 'WP_Terms_List_Table' ) // для подсчета количества
	)
		return $pieces;

	// дополним запрос
	global $wpdb;
	if( @ $_GET['subject'] ){
		$pieces['join'] .= " LEFT JOIN $wpdb->termmeta AS tm ON t.term_id = tm.term_id ";
		$pieces['where'] .= " AND tm.meta_key = 'ss_subject' AND tm.meta_value = ". intval($_GET['subject']) ." ";
	}
	if( @ $_GET['grade'] ){
		$pieces['join'] .= " LEFT JOIN $wpdb->termmeta AS tm2 ON t.term_id = tm2.term_id ";
		$pieces['where'] .= " AND tm2.meta_key = 'ss_grade' AND tm2.meta_value = ". intval($_GET['grade']) ." ";
	}

	return $pieces;
}
Свои фильтры в таблицах записей, комментариев, пользователей, таксономий 2 комментария
  • Александр

    Здравствуйте статья интересная, спасибо. Вопрос негде не могу найти инфу как сделать фильтры записей не в админ панели а для посетителей сайта, то есть добавляешь произвольные поля туда какие не будь числовые значения, а потом как сделать фильтрацию по этим произвольным полям. Спасибо !

  • Здравствуйте.
    Не получилось реализовать первый пример этой статьи: Фильтр в таблице записей.
    Переделал код и пошло хотя до полного понимания далеко.
    Вот что получилось:

    add_action( 'restrict_manage_posts', 'add_event_table_filters');
    function add_event_table_filters( $post_type ){
    // убедимся что мы на нужной странице админки иначе во все пост-тайпы добавит
    $cs = get_current_screen();
    	if( empty($cs->post_type) || $cs->post_type != 'event' || $cs->id != 'edit-event' ) return;
    	echo '
    	<select name="sel_season">
    		<option value="-1">- все сезоны -</option>
    		<option value="203" '. selected(203, @ $_GET['sel_season'], 0) .'>2016-2017</option>
    		<option value="83"'.   selected(83, @ $_GET['sel_season'], 0) .'>2015-2016</option>
    		<option value="154"'.  selected(154, @ $_GET['sel_season'], 0) .'>2014-2015</option>
    	</select>';
    }
    
    add_action( 'pre_get_posts', 'add_event_table_filters_handler' );
    function add_event_table_filters_handler( $query ) {
    if( ! is_admin() ) return; // выходим если не админка
    //фикс, необходимо т.к. ф-я 'get_current_screen' не опредлена на страницах
    // Внешний вид -> Настроить (возможно еще есть какие-то) и будет выдавать ошибку
    	if (!function_exists('get_current_screen')) return;  
    	$cs = get_current_screen();
    	if( empty($cs->post_type) || $cs->post_type != 'event' || $cs->id != 'edit-event' ) return;
    
    	if(@ $_GET['sel_season'] )            //фикс, убрать проблему первого появления таблицы
    	if( @ $_GET['sel_season'] != -1 ){
    		$selected_id = @ $_GET['sel_season'];
    		$query->set( 'tax_query', array([ 'taxonomy'=>'season','field'  => 'slug', 'terms'=>$selected_id ]) );
    	}
    }

    так и не понял для чего этот кусок куда, без него хорошо smile :

    // if( empty($_GET['orderby']) && @ $_GET['sel_season'] != -1 ){
    	//  $query->set( 'orderby', 'menu_order date' );
    	// }

    в запросе не хватает кусочка 'field' => 'slug'

    $query->set( 'tax_query', array([ 'taxonomy'=>'season','field'  => 'slug', 'terms'=>$selected_id ]) );
    Ответить4 месяца назад #

Здравствуйте, !

Ваш комментарий