Секреты WP_Query

На четвёртом митапе я рассказывал о том, что такое WP_Query, глобальные объекты $wp_query и $wp_the_query, чем опасен query_posts, как правильно делать вторичные циклы и изменять основной цикл, и многое многое другое. Сегодня я наконец-то решил выложить слайды с презентации а так же мои записульки. Если будут вопросы не стесняйтесь!

Чтобы упростить код и уменьшить его объём, некоторые функции и конструкции были изменены, поэтому не удивляйтесь, если вдруг где-нибудь не хватает аргументов, или объявления глобальной переменной.

Начнём с простого — коротко о WP_Query. Это класс, который позволяет нам получать контент из базы данных WordPress. Есть так же глобальный объект этого класса, который называется $wp_query, но о нём чуть позже. Чаще всего мы рабоаем с WP_Query через следующие конструкции:

if ( have_posts() )
    while ( have_posts() )
        the_post();

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

query_posts( 'cat=-5' );
$posts = get_posts( 'cat=-5' );
$posts = new WP_Query( 'cat=-5' );

Причём первый метод используется чаще всего, потому что он кажется чуть проще остальных. В любом случае, каждый из этих методов является вторичным запросом, а первичный запрос уже сделан. Первичный — это тот, который WordPress сформировал из запрошенного адреса (URL) ещё до того, как он подгрузил ваш шаблон.

Соответственно WordPress сначала собрал список из 10 последних постов на главную, а затем собрал ещё список из 10 постов на главную, только в этот раз исключил одну категорию. Двойная работа. Но дело-то и не в двойной работе.

Многие вещи в WordPress, завязаны вокруг этого самого главного запроса, и игнорировать его нельзя. Например, как часто мы сталкиваемся с вопросом «а почему пагинация не работает?» Так если в главном запросе найдено всего три страницы, а в вашем вторичном запросе найдено пять, то на четвёртой и пятой страницах вы скорее всего увидите ошибку. И это потому что WordPress посчитает что таких страниц нет, основываясь на главном запросе, и подгрузит ваш шаблон 404.php. До вашего вторичного запроса в index.php дело так и не дойдёт.

Как это всё исправить?

Событие pre_get_posts

Событие pre_get_posts происходит внутри WP_Query и во время него, мы можем изменять запрос. Например, убрать 5-ю категорию из запроса:

add_action( 'pre_get_posts', 'my_pre_get_posts' );
function my_pre_get_posts( $query ) {
    $query->set( 'cat', '-5' );
}

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

$query->is_main_query()

Соответственно:

add_action( 'pre_get_posts', 'my_pre_get_posts' );
function my_pre_get_posts( $query ) {
    if ( $query->is_main_query() )
        $query->set( 'cat', '-5' );
}

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

add_action( 'pre_get_posts', 'my_pre_get_posts' );
function my_pre_get_posts( $query ) {
    if ( $query->is_main_query() && $query->is_search() )
        $query->set( 'post_type', 'post' );
}

Можем для поисковых страниц выводить по тридцать записей на страницу, а не по десять:

add_action( 'pre_get_posts', 'my_pre_get_posts' );
function my_pre_get_posts( $query ) {
    if ( $query->is_main_query() && $query->is_search() )
        $query->set( 'posts_per_page', 30 );
}

При этом пагинация будет работать! Ещё один пример: выводить наш кастомный тип постов (Custom Post Type) в списке с остальными на главной странице:

add_action( 'pre_get_posts', 'my_pre_get_posts' );
function my_pre_get_posts( $query ) {
    if ( $query->is_main_query() && $query->is_home() )
        $query->set( 'post_type', array( 'post', 'book' );
}

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

Вторичные запросы

Напоминаю, вторичные запросы, это следующие методы:

query_posts();
$posts = get_posts();
$posts = new WP_Query();

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

$popular = new WP_Query( 'category_name=popular' );
while ( $popular->have_posts() ) {
    $popular->the_post();
    ...
}

// Основной запрос!
while ( have_posts() ) {
    the_post();
    ...
}

При этом, ваш основной запрос никуда не делася! Ещё один пример, чуть сложнее. После каждого поста, покажем случайным образом пост из той же категории:

// Основной запрос
while ( have_posts() ) {
    the_post();
    ...

    // Вторичный запрос
    $category = get_the_category();
    $related = new WP_Query( 'cat=' . $category[0]->term_id . '&orderby=rand' );
    while ( $related->have_posts() ) {
        $related->the_post();
	...
    }
}

Что такое query_posts?

Вот здесь нужно аккуратно и внимательно. В WordPress есть глобальный объект $wp_query, который хранит в себе основной запрос (ну почти…) А функции have_posts, the_post и т.д. используют этот глобальный объект:

function have_posts() {
    global $wp_query;
    return $wp_query->have_posts();
}

На самом деле, глобальный объект $wp_query является всего лишь ссылкой на объект $wp_the_query который (честное слово!) хранит в себе основной запрос.

$wp_query =& $wp_the_query;

А функция query_posts ломает эту ссылку, и изменяет глобальный объект $wp_query на ваш новый, вторичный запрос.

function &query_posts( $query ) {
    ...
    unset( $wp_query );
    $wp_query = new WP_Query();
    return wp_query->query( $query );
}

То есть функцией query_posts, мы создаём вторичный запрос, но впечатление у нас того, что мы изменили главный запрос, потому что мы теперь можем пользоваться функциями have_posts, the_post и т.д. напрямую, с нашим новым запросом.

При всём этом, первичный запрос, никуда не делся, и вернуть его в глобальный объект $wp_query, можно с помощью функции wp_reset_query:

function wp_reset_query() {
    ...
    unset( $wp_query );
    $wp_query =& $wp_the_query;
}

Которая восстанавливает ссылку на объект $wp_the_query, после чего, функции have_posts и т.д. будут снова оперировать над настоящим основным запросом.

На примере с популярными постами. Вот что было при использовании нового объекта класса WP_Query:

// Вторичный запрос
$popular = new WP_Query( 'category_name=popular' );
while ( $popular->have_posts() ) {
    $popular->the_post();
    ...
}

// Основной запрос
while ( have_posts() ) {
    the_post();
    ...
}

И вот, что стало, при правильном использовании query_posts:

// Вторичный запрос
query_posts( 'category_name=popular' );
while ( have_posts() ) {
    the_post();
    ...
}
wp_reset_query();

// Основной запрос
while ( have_posts() ) {
    the_post();
    ...
}

Подведём итоги:

  • Если нужно изменить основной запрос, pre_get_posts
  • Если нужен вторичный запрос: new WP_Query, get_posts
  • Если нужна головная боль: query_posts