WordPress как на ладони
WordCamp Saint Petersburg 2018 wordpress jino

pre_get_posts хук-событие . WP 2.0

Событие срабатывает перед каждым запросом WP_Query. До того, как был сделан запрос в базу. Используется для изменения запроса.

Событие pre_get_posts дает возможность изменять объект $wp_query, потому что объект передается в хук по ссылке (&), это означает, что любые действия над $query внутри функции буду влиять на основной объект $wp_query. Для этого хука не надо возвращать никаких данных.

ВАЖНО! pre_get_posts срабатывает для каждого запроса WP_Query: основной запрос, дополнительный запрос, запрос в админ-панели, запросы в виджетах и т.д.

Поэтому нужно убедитесь, что вы изменяете именно нужны вам запрос, для этого перед изменением запроса используйте всевозможные Условные теги, чтобы точно ограничить изменение (см. примеры).

Часто используемые функции внутри хука pre_get_posts:

Использование

add_action( 'pre_get_posts', 'action_function_name_11' );
function action_function_name_11( $query ) {
	// Действия...
}

Параметры

$query(объект)
Объект WP_Query.

Заметки

Аргумент передается по ссылке

Объект $query предается по ссылке, поэтому нет необходимости определять глобальную переменную. Любые изменения $query внутри функции влияют сразу на оригинальный объект.

Запросы постоянных страниц

pre_get_posts нельзя использовать для изменения запросов связанных с постоянными страницами, потому что 'is_page', 'is_singular', 'pagename' и другие параметры (зависимые к ЧПУ) уже установлены в объекте. Рекомендуется использовать new WP_Query в шаблоне постоянной страницы, чтобы изменить запрос.

Определение нужного запроса

При использовании pre_get_posts, нужно точно определить, в какой конкретно запрос вы вносите изменения. Полезный метод для этого: $query->is_main_query() - он поможет вам убедиться, что вносимые изменения будут влиять только на основные запросы. Используйте его в связке с условными тегами. Чтобы изменять запрос только для страниц, которые вам нужны.

Например, мы хотим изменить запрос на странице категорий и не делаем проверку is_category(), тогда наши изменения будут влиять на формирования запроса в админ-панели, на других страницах сайта и где угодно еще. Поэтому четко определяйте, для какого запроса вы вносите изменения через действие-хук pre_get_posts.

Использование в админ-панели

Этот фильтр можно также использовать для изменения запросов в админ-панели. В таких случаях, убедитесь что внесенные изменения будут работать на странице вывода записей. Например, проверка $query->is_main_query() и is_post_type_archive('movie') (изменяем запрос для лицевой части для записей типа movie), изменит также запрос на страницы edit.php?post_type=movie. Чтобы этого не произошло, нужно использовать еще проверку ! is_admin().

Внимание! Условные теги

pre_get_posts срабатывает до того, как объект WP_Query полностью определится. Поэтому некоторые условные теги, опирающиеся на данные WP_Query еще не работают. Например, is_front_page() не работает, тогда как is_home() будет работать. Поэтому вам лучше работать с данными объекта напрямую, например: $query->is_search.

Весь список свойств которые можно использовать вместо условного тега:

$query->is_404
$query->is_admin
$query->is_archive
$query->is_attachment
$query->is_author
$query->is_category
$query->is_comments_popup
$query->is_comment_feed
$query->is_date
$query->is_day
$query->is_feed
$query->is_home
$query->is_month
$query->is_page
$query->is_paged
$query->is_posts_page
$query->is_post_type_archive
$query->is_preview
$query->is_robots
$query->is_search
$query->is_single
$query->is_singular
$query->is_tag
$query->is_tax
$query->is_time
$query->is_trackback
$query->is_year

// функции
$query->is_front_page()
$query->is_main_query()

Все остальные условные теги нужно заменить проверкой или методом класса. Например, is_front_page(), нужно заменить на такую проверку:

if( $query->is_home || ($query->get('page_id') == get_option('page_on_front')) ){
	// это front_page
}

или такой метод:

if( $query->is_front_page() ){
	// это front_page
}

is_main_query() не рекомендуется использовать внутри этого хука. Нужно отдать предпочтение методу $query->is_main_query().

Отступы и пагинация

Использование аргумента offset в любом запросе может сломать пагинацию. Поэтому, если вы используете offset, то вам нужно изменить запрос для всех страниц пагинации, опираясь на первоначальный отступ (offset). Подробнее об этом я писал в этой статье: Как использовать параметр offset не ломая пагинацию.

Примеры

#1 Исключение категорий на главной

Этот пример показывает как убрать посты из указанных категорий из вывода на главной странице блога. Например, у нас есть 2 категории с ID 1 и 1347, которые нам не нужно показывать на главной. Чтобы исключить эти категории из запроса, используйте такой код в плагине или в теме:

function exclude_category( $query ) {
	if ( $query->is_front_page() && $query->is_main_query() ) {
		$query->set( 'cat', '-1,-1347' );
	}
}
add_action( 'pre_get_posts', 'exclude_category' );

#2 Исключение постоянных страниц из результатов поиска

Когда пользователи вашего сайта ищут что-либо, часто в результатах поиска могут попадаться постоянные страницы, которые в принципе совсем не нужны в результатах поиска, и которые можно исключить из поиска насовсем. Используйте хук pre_get_posts, чтобы исключить из результатов поиска постоянных страницы:

function search_filter($query) {
  if ( ! is_admin() && $query->is_main_query() ) {
	if ($query->is_search) {
	  $query->set('post_type', 'post');
	}
  }
}

add_action( 'pre_get_posts', 'search_filter' );

#3 Включение произвольного типа записей в результаты поиска

Включать в поиск произвольный тип записи или нет, устанавливается при регистрации типа записи, в аргументах функции register_post_type(): аргумент public=true добавляет в результат поиска произвольный тип записи.

Если, произвольный тип не включен в поиск, но нужно чтобы он участвовал в поиске, то используйте такой код, аналог предыдущего:

add_action('pre_get_posts', 'get_posts_search_filter');
function get_posts_search_filter( $query ){
	if ( ! is_admin() && $query->is_main_query() && $query->is_search ) {
		$query->set('post_type', array('post', 'movie') );
	}
}

#4 Изменение количества выводимых на странице постов

В WordPress есть глобальная настройка, которая учитывает сколько записей показывать на странице - posts_per_page. Лучше всего изменять этот параметр до основного запроса, в целях экономии ресурсов, чтобы не делать повторных запрос. Так, мы можем использовать хук-действие pre_get_posts, чтобы изменить количество выводимых записей на странице.

Этот пример, показывает как перезаписать параметр posts_per_page для страницы архивов произвольного типа записи movie:

add_action('pre_get_posts', 'hwl_home_pagesize', 1 );
function hwl_home_pagesize( $query ) {
	// Выходим, если это админ-панель или не основной запрос.
	if( is_admin() || ! $query->is_main_query() )
		return;

	if( is_home() ){
		// Выводим только 1 пост на главной странице
		$query->set( 'posts_per_page', 1 );
	}

	if( $query->is_post_type_archive('movie') ){
		// Выводим 50 записей если это архив типа записи 'movie'
		$query->set( 'posts_per_page', 50 );
	}
}

Пример объекта WP_Query

Для того, чтобы быстро разобраться как и что использовать, ниже пример объекта WP_Query (global $wp_query), который передается в хук по ссылке (&$query):

WP_Query Object
(
	[query_vars] => Array
		(
			[page] => 0
			[pagename] => s
			[error] => 
			[m] => 
			[p] => 0
			[post_parent] => 
			[subpost] => 
			[subpost_id] => 
			[attachment] => 
			[attachment_id] => 0
			[name] => s
			[static] => 
			[page_id] => 0
			[second] => 
			[minute] => 
			[hour] => 
			[day] => 0
			[monthnum] => 0
			[year] => 0
			[w] => 0
			[category_name] => 
			[tag] => 
			[cat] => 
			[tag_id] => 
			[author] => 
			[author_name] => 
			[feed] => 
			[tb] => 
			[paged] => 0
			[comments_popup] => 
			[meta_key] => 
			[meta_value] => 
			[preview] => 
			[s] => 
			[sentence] => 
			[fields] => 
			[menu_order] => 
			[category__in] => Array
				(
				)

			[category__not_in] => Array
				(
				)

			[category__and] => Array
				(
				)

			[post__in] => Array
				(
				)

			[post__not_in] => Array
				(
				)

			[tag__in] => Array
				(
				)

			[tag__not_in] => Array
				(
				)

			[tag__and] => Array
				(
				)

			[tag_slug__in] => Array
				(
				)

			[tag_slug__and] => Array
				(
				)

			[post_parent__in] => Array
				(
				)

			[post_parent__not_in] => Array
				(
				)

			[author__in] => Array
				(
				)

			[author__not_in] => Array
				(
				)

			[ignore_sticky_posts] => 
			[suppress_filters] => 
			[cache_results] => 
			[update_post_term_cache] => 1
			[update_post_meta_cache] => 1
			[post_type] => 
			[posts_per_page] => 10
			[nopaging] => 
			[comments_per_page] => 10
			[no_found_rows] => 
			[order] => DESC
		)

	[tax_query] => 
	[meta_query] => WP_Meta_Query Object
		(
			[queries] => Array
				(
				)

			[relation] => 
		)

	[date_query] => 
	[post_count] => 1
	[current_post] => -1
	[in_the_loop] => 
	[comment_count] => 0
	[current_comment] => -1
	[found_posts] => 1
	[max_num_pages] => 0
	[max_num_comment_pages] => 0
	[is_single] => 
	[is_preview] => 
	[is_page] => 1
	[is_archive] => 
	[is_date] => 
	[is_year] => 
	[is_month] => 
	[is_day] => 
	[is_time] => 
	[is_author] => 
	[is_category] => 
	[is_tag] => 
	[is_tax] => 
	[is_search] => 
	[is_feed] => 
	[is_comment_feed] => 
	[is_trackback] => 
	[is_home] => 
	[is_404] => 
	[is_comments_popup] => 
	[is_paged] => 
	[is_admin] => 
	[is_attachment] => 
	[is_singular] => 1
	[is_robots] => 
	[is_posts_page] => 
	[is_post_type_archive] => 
	[query_vars_hash] => c248b13e251e8fba33892e0bd7a5bd98
	[query_vars_changed] => 
	[thumbnails_cached] => 
	[stopwords:WP_Query:private] => 
	[query] => Array
		(
			[page] => 
			[pagename] => s
		)

	[queried_object] => WP_Post Object
		(
			[ID] => 19
			[post_author] => 1
			[post_date] => 2010-04-01 20:09:20
			[post_date_gmt] => 2010-04-01 16:09:20
			[post_content] => 
			[post_title] => Страница для исполнения
			[post_excerpt] => 
			[post_status] => private
			[comment_status] => open
			[ping_status] => closed
			[post_password] => 
			[post_name] => s
			[to_ping] => 
			[pinged] => 
			[post_modified] => 2011-04-25 17:44:42
			[post_modified_gmt] => 2011-04-25 13:44:42
			[post_content_filtered] => 
			[post_parent] => 0
			[guid] => http://site.ru/s
			[menu_order] => 0
			[post_type] => page
			[post_mime_type] => 
			[comment_count] => 0
			[filter] => raw
		)

	[queried_object_id] => 19
	[request] => SELECT   wp_posts.* FROM wp_posts  WHERE 1=1  
				 AND (wp_posts.ID = '19') AND wp_posts.post_type = 'page'  
				 ORDER BY wp_posts.post_date DESC 
	[posts] => Array
		(
			[0] => WP_Post Object
				(
					[ID] => 19
					[post_author] => 1
					[post_date] => 2010-04-01 20:09:20
					[post_date_gmt] => 2010-04-01 16:09:20
					[post_content] => 
					[post_title] => Страница для исполнения
					[post_excerpt] => 
					[post_status] => private
					[comment_status] => open
					[ping_status] => closed
					[post_password] => 
					[post_name] => s
					[to_ping] => 
					[pinged] => 
					[post_modified] => 2011-04-25 17:44:42
					[post_modified_gmt] => 2011-04-25 13:44:42
					[post_content_filtered] => 
					[post_parent] => 0
					[guid] => http://site.ru/s
					[menu_order] => 0
					[post_type] => page
					[post_mime_type] => 
					[comment_count] => 0
					[filter] => raw
				)

		)

	[post] => WP_Post Object
		(
			[ID] => 19
			[post_author] => 1
			[post_date] => 2010-04-01 20:09:20
			[post_date_gmt] => 2010-04-01 16:09:20
			[post_content] => 
			[post_title] => Страница для исполнения
			[post_excerpt] => 
			[post_status] => private
			[comment_status] => open
			[ping_status] => closed
			[post_password] => 
			[post_name] => s
			[to_ping] => 
			[pinged] => 
			[post_modified] => 2011-04-25 17:44:42
			[post_modified_gmt] => 2011-04-25 13:44:42
			[post_content_filtered] => 
			[post_parent] => 0
			[guid] => http://site.ru/s
			[menu_order] => 0
			[post_type] => page
			[post_mime_type] => 
			[comment_count] => 0
			[filter] => raw
		)

)

Где используется хук

WP_Query::get_posts() остальные хуки:

Код хука-события pre_get_posts

Фрагмент из: wp-includes/class-wp-query.php VER 4.9.6
...
		global $wpdb;

		$this->parse_query();

		/**
		 * Fires after the query variable object is created, but before the actual query is run.
		 *
		 * Note: If using conditional tags, use the method versions within the passed instance
		 * (e.g. $this->is_main_query() instead of is_main_query()). This is because the functions
		 * like is_main_query() test against the global $wp_query instance, not the passed one.
		 *
		 * @since 2.0.0
		 *
		 * @param WP_Query $this The WP_Query instance (passed by reference).
		 */
		do_action_ref_array( 'pre_get_posts', array( &$this ) );

		// Shorthand.
		$q = &$this->query_vars;

		// Fill again in case pre_get_posts unset some vars.
		$q = $this->fill_query_vars($q);

		// Parse meta query
		$this->meta_query = new WP_Meta_Query();
		$this->meta_query->parse_query_vars( $q );

		// Set a flag if a pre_get_posts hook changed the query vars.
		$hash = md5( serialize( $this->query_vars ) );
		if ( $hash != $this->query_vars_hash ) {
			$this->query_vars_changed = true;
...
pre_get_posts 23 коммента
Вопросы 3 Все
  • @ Печник Роман cайт: kladka-kamina.ru

    Изменение количества выводимых на странице постов,
    В новой версии WP 3.8 не работает, может есть какой другой способ

    1
    Ответить4.3 года назад #
  • campusboy2782 cайт: www.youtube.com/c/wpplus

    Как правильно будет исключить посты с определенным произвольным полем?

    Ответить1.6 год назад #
    • Kama5287

      Через meta_query норм:

      $query->set( 'meta_query', array([ 'key'=>'__ПОЛЕ__', 'compare'=>'NOT EXISTS' ]); );
      2
      Ответить1.6 год назад #
  • valdus

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

    function SearchExlude($query) 
    {    
     if ($query->is_search) 
      { $query->set('cat','-27'); }    
     return $query;
     }
    add_filter('pre_get_posts','SearchExlude');

    Нашел такое решение проблемы, но столкнулся с другой.
    Этот методо исключает полностью из поиска данную рубинку (из первого и второго).
    Как можно решить проблему, подскажите.

    Первый поиск находится в шапке сайта, второй в сайдбаре

    Ответить1.5 год назад #
    • campusboy2782 cайт: www.youtube.com/c/wpplus

      Приветствую. Передавайте в форме ещё один параметр в дополнение к поисковому запросу, к примеру, по которому уже можно решить, в каком случае исключать одно, а в каком другое.

      1
      Ответить1.5 год назад #
    • Kama5287

      Если эти разные поиски выводятся на разных страницах, то campusboy вам ответил уже.

      Если оба поиска нужно вывести на одной странице в разных блоках. То вам нужно создать 2 запроса. Первый пусть будет основной и в нем вы уже исключили рубрику 27. А второй создавайте с помощью WP_Query там укажите параметр s и рубрику 27, т.е. искать только в рубрике 27...

      Ответить1.5 год назад #
  • Сергей

    Здравствуйте. Мне нужно при выводе архива категории добавить посты из другой категории. Достаточно ли установить category__in или надо ещё что-нибудь сделать (сбросить cat, category_name)?

    • Сергей

      Вообще при выводе архива категории изменение cat или category__in приводит к тому, что "записи не найдены". Как через pre_get_posts правильно вывести архив двух категорий?

  • @ Эдуард cайт: i-advocat.ru

    Здравствуйте, Тимур.
    Мне нужно, чтобы для пользователя с определенной ролью в админке отображались записи пользовательского типа только из определенного соответствующего ему региона.
    Для этого я на этапе регистрации присваиваю пользователю мета-поле "'region', а потом при сохранении каждого поста сохраняю его значение в мета-поле поста из мета-поля пользователя.
    Далее для того, чтобы пользователю в админке отображались записи типа 'practice' только из его региона, я пишу:

    add_action('pre_get_posts', 'livepravo_chamber_practice_table', 99, 1);
    function livepravo_chamber_practice_table($query) {
    if (!$query->is_admin) return; // Если не админнка, нахер с пляжа.
    if (!wp_get_current_user()->has_cap('chamber')) return; // Если не нужная роль- аналогично.
    $query->set('meta_key', 'region');
    $query->set('meta_value', wp_get_current_user()->region);
    } // End function

    Теперь два вопроса:

    1. Как мне сделать, чтобы эти записи и из корзины не выбирались? То есть, чтобы на экране "Все записи типа 'practice'" показывало в корзине количество записей с учетом изложенного условия. Сейчас показывает количество всех записей, без учета региона. Реально, так и пишет: Всего записей 4, включая две опубликованные, два черновика и еще в корзине 28.

    2. Можно ли действие хука pre_get_posts() ограничить только конкретным экраном с таблицей постов, Вместо:
      if (get_current_screen()->id != 'edit-practice') return;
      Объект экрана на этапе выполнения этого хука, как я понял, не доступен (PHP пишет, что функция не определена), Но может есть какой-то обходной вариант?
  • @ Сёмка cайт: site-style.by

    Доброй ночи.
    Может подскажете, как можно через pre_get_posts вывести посты определенного автора и посты из определенной категории?
    Не посты определенного автора в определенной категории, а именно определенного автора и из определенной категории

    1
  • Роман cайт: bro-dev.tk

    Насколько плохо делать вот так?

    add_action('pre_get_posts', 'myPreGetPosts', 10, 1);
    $the_query = new WP_Query($query_string . '&posts_per_page=5&tag_id=4232');
    remove_action('pre_get_posts', 'myPreGetPosts', 10, 1);

    Какие есть альтернативы?

    Ответить4 месяца назад #
    • Kama5287

      Так можно делать, ничего страшного. А чтобы дать норм совет, нужно понимать задачу. В целом ничего плохого в таком подходе нет, единственное только как-то не очень логично он выглядит...

      Ответить4 месяца назад #
  • Использую pre_get_posts, чтобы ограничить кол-во результатов стандартного поиска WP, т.е. нужна одна страница, без пагинации. Но не вижу полностью корректного способа. Варианта нашёл только 2:
    1) posts_per_page = N + скрыть пагинацию при помощи условного тега
    2) вывести все посты при помощи nopaging = true или posts_per_page = -1

    Но оба эти варианта - костыли. Хотя если создать экземпляр WP_Query напрямую, то там есть свойство post_count, которое отлично бы мне подошло, но его нельзя использовать при правке через pre_get_posts. Может подскажите что-то дельное, чего я пока не заметил?

    Ответить20 дней назад #
    • @ Сёмка cайт: site-style.by

      Наилучший и самый оптимальный вариант - это использование хука для pre_get_posts.
      Например:

      function search_filter($query) {
        if ( !is_admin() && $query->is_main_query() ) {
      	if ($query->is_search) {
      	  $query->set('posts_per_page', '-1');
      	}
        }
      }
      
      add_action('pre_get_posts','search_filter');
      Ответить20 дней назад #
      • Спасибо за совет, но у меня этот вариант указан в п2. Не хочется, чтобы в результатах выводилось по несколько десятков постов, не говоря уже о большем кол-ве.

        Нашёл казалось бы верный вариант с хуком post_limits, но после его применения пагинация, создаваемая через paginate_links всё равно выводится, хотя она же вроде бы как использует тот же экземпляр $wp_query. Тут у меня, видимо, пробел в знаниях, буду искать причину такого поведения.

        Ответить19 дней назад #
        • @ Сёмка cайт: site-style.by

          Вместо -1 напишите цифру и у вас будет выводиться количество постов, которые надо вывести

          -1
          Ответить19 дней назад #
    • Kama5287

      Тебе надо указать параметр no_found_rows = 1 и posts_per_page = N. Тогда пагинации не должно быть...

      Ответить7 дней назад #
      • Спасибо за совет, но мне эти оба варианта не подошли, т.к. в результатах поиска использую $wp_query->found_posts для вывода кол-ва найденных, а:
        при no_found_rows = 1 результатов 0
        при posts_per_page = 5 результатов сколько нашлось, пагинация осталась

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

        function search_found_posts( $found_posts, $query ) {
        
        	if ( is_search() AND ! is_admin() ) {
        
        		if ( $query->get( 'posts_per_page' ) < $query->found_posts ) {
        
        			$found_posts = $query->get( 'posts_per_page' );
        		}
        	}
        
        	return $found_posts;
        }
        add_filter( 'found_posts', 'search_found_posts', 10, 2 );
        Ответить5 дней назад #
        • Kama5287

          Не понял, а зачем ты юзаешь $wp_query->found_posts, если тебе нужна в результатах показать сколько показано на странице можно же просто посчитать элементы массива: count( $wp_query->posts ). Просто с no_found_rows не будет таких вот костылей и запрос будет полегче...

          Ответить4 дня назад #

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