Когда использовать WP_query(), query_posts() и pre_get_posts
Вчера я прочитал статью @nacin "Вы не знаете Query" и погрузился в кроличью нору запросов. До вчерашнего дня я (неправильно) использовал query_posts()
для всех моих потребностей в запросах. Теперь я немного лучше понимаю использование WP_Query()
, но всё еще есть некоторые серые зоны.
Что, как мне кажется, я знаю наверняка:
Если я создаю дополнительные циклы где-либо на странице — в сайдбаре, в футере, любые "похожие записи" и т.д. — я должен использовать WP_Query()
. Я могу использовать его многократно на одной странице без вреда (верно?).
Что я не знаю наверняка
- Когда использовать @nacin's
pre_get_posts
вместоWP_Query()
? Должен ли я теперь использоватьpre_get_posts
для всего? - Когда я хочу изменить цикл на странице шаблона — допустим, я хочу модифицировать страницу архива таксономии — должен ли я удалить часть
if have_posts : while have_posts : the_post
и написать свой собственныйWP_Query()
? Или мне следует изменить вывод, используяpre_get_posts
в моем файле functions.php?
кратко
Краткие правила, которые я хотел бы вывести из этого:
- Больше никогда не использовать
query_posts
- При выполнении нескольких запросов на одной странице использовать
WP_Query()
- При модификации цикла делать __________________.
Спасибо за любую мудрость
Терри
PS: Я видел и читал: Когда следует использовать WP_Query вместо query_posts() или get_posts()? Что добавляет еще одно измерение — get_posts
. Но совсем не рассматривает pre_get_posts
.

Вы совершенно правы, говоря:
Никогда больше не используйте
query_posts
pre_get_posts
pre_get_posts
— это фильтр для изменения любого запроса. Чаще всего он используется для изменения только «главного запроса»:
add_action('pre_get_posts','wpse50761_alter_query');
function wpse50761_alter_query($query){
if( $query->is_main_query() ){
//Делаем что-то с главным запросом
}
}
(Я бы также проверил, что is_admin()
возвращает false — хотя это может быть избыточно.) Главный запрос появляется в ваших шаблонах как:
if( have_posts() ):
while( have_posts() ): the_post();
//Цикл
endwhile;
endif;
Если вам когда-нибудь захочется изменить этот цикл — используйте pre_get_posts
. То есть, если у вас возникнет соблазн использовать query_posts()
— используйте вместо этого pre_get_posts
.
WP_Query
Главный запрос — это важный экземпляр объекта WP_Query
. WordPress использует его, например, для определения того, какой шаблон использовать, и все аргументы, переданные в URL (например, пагинация), направляются в этот экземпляр объекта WP_Query
.
Для дополнительных циклов (например, в боковых панелях или списках «похожих записей») вам нужно создать свой собственный отдельный экземпляр объекта WP_Query
. Например:
$my_secondary_loop = new WP_Query(...);
if( $my_secondary_loop->have_posts() ):
while( $my_secondary_loop->have_posts() ): $my_secondary_loop->the_post();
//Дополнительный цикл
endwhile;
endif;
wp_reset_postdata();
Обратите внимание на wp_reset_postdata();
— это связано с тем, что дополнительный цикл переопределит глобальную переменную $post
, которая идентифицирует «текущую запись». По сути, это сбрасывает её к той записи $post
, на которой мы находимся.
get_posts()
По сути, это обёртка для отдельного экземпляра объекта WP_Query
. Она возвращает массив объектов записей. Методы, используемые в цикле выше, больше не доступны. Это не «цикл», а просто массив объектов записей.
<ul>
<?php
global $post;
$args = array( 'numberposts' => 5, 'offset'=> 1, 'category' => 1 );
$myposts = get_posts( $args );
foreach( $myposts as $post ) : setup_postdata($post); ?>
<li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
<?php endforeach; wp_reset_postdata(); ?>
</ul>
В ответ на ваши вопросы
- Используйте
pre_get_posts
для изменения главного запроса. Используйте отдельный объектWP_Query
(способ 2) для дополнительных циклов в шаблонных страницах. - Если вы хотите изменить запрос главного цикла, используйте
pre_get_posts
.

Есть ли сценарии, когда лучше сразу использовать get_posts() вместо WP_Query?

@drtanz - да. Например, если вам не нужна пагинация или закрепленные записи вверху - в этих случаях get_posts()
будет более эффективным.

Но разве это не добавит дополнительный запрос, когда мы могли бы просто модифицировать pre_get_posts для изменения основного запроса?

@drtanz - вы не должны использовать get_posts()
для основного запроса - это для вторичных запросов.

В вашем примере с WP_Query, если вы замените $my_secondary_loop->the_post(); на $my_post = $my_secondary_loop->next_post();, то можете избежать необходимости помнить о wp_reset_postdata(), при условии что вы используете $my_post для выполнения нужных действий.

@Privateer Это не так, WP_Query::get_posts()
устанавливает global $post;

@StephenHarris Я только что просмотрел класс запроса и не нашел этого. Проверил в основном потому, что никогда не использую wp_reset_postdata, так как всегда делаю запросы таким способом. Вы создаете новый объект, и все результаты содержатся внутри него.

@Privateer - извините, опечатка, WP_Query::the_post()
, см.: https://github.com/WordPress/WordPress/blob/759f3d894ce7d364cf8bfc755e483ac2a6d85653/wp-includes/query.php#L3732

@StephenHarris Верно =) Если вы используете next_post() для объекта вместо the_post, вы не затрагиваете глобальный запрос и вам не нужно помнить о необходимости использовать wp_reset_postdata после этого.

@Privateer Ох, мои извинения, кажется, я сам себя запутал. Ты прав (но ты не сможешь использовать функции, которые ссылаются на глобальную переменную $post
, например the_title()
, the_content()
).

Верно =) Я никогда не использую эти функции, поэтому мне их и не хватает.

@urok93 Иногда я использую get_posts()
, когда мне нужно получить связанные записи через ACF, особенно если там только одна запись. Хотя для стандартизации шаблонов я рассматриваю возможность переписать их как экземпляры WP_Query.

Существует два различных контекста для циклов:
- основной цикл, который выполняется на основе URL-запроса и обрабатывается перед загрузкой шаблонов
- вторичные циклы, которые выполняются любым другим способом, вызываются из файлов шаблонов или иным образом
Проблема с query_posts()
заключается в том, что это вторичный цикл, который пытается быть основным и терпит неудачу. Поэтому забудьте о его существовании.
Как изменить основной цикл
- не используйте
query_posts()
- используйте фильтр
pre_get_posts
с проверкой$query->is_main_query()
- альтернативно используйте фильтр
request
(слишком грубый, поэтому предыдущий вариант лучше)
Как выполнить вторичный цикл
Используйте new WP_Query
или get_posts()
, которые практически взаимозаменяемы (последний является тонкой обёрткой для первого).
Как очистить
Используйте wp_reset_query()
, если вы использовали query_posts()
или напрямую изменяли глобальную переменную $wp_query
— так что вам почти никогда это не понадобится.
Используйте wp_reset_postdata()
, если вы использовали the_post()
или setup_postdata()
, или изменяли глобальную переменную $post
и вам нужно восстановить исходное состояние пост-данных.

Существуют законные сценарии использования query_posts($query)
, например:
Вы хотите отобразить список записей или записей пользовательского типа поста на странице (используя шаблон страницы)
Вы хотите, чтобы работала пагинация этих записей
Теперь, зачем вам может понадобиться отображать это на странице вместо использования шаблона архива?
Это более интуитивно понятно для администратора (вашего клиента?) — они могут видеть страницу в разделе 'Страницы'
Это удобнее для добавления в меню (без страницы им пришлось бы добавлять URL напрямую)
Если вы хотите отобразить дополнительный контент (текст, миниатюру записи или любой пользовательский мета-контент) в шаблоне, вы можете легко получить его со страницы (и это также более понятно для клиента). Если бы вы использовали шаблон архива, вам пришлось бы либо жестко прописывать дополнительный контент, либо использовать, например, настройки темы/плагина (что менее интуитивно для клиента)
Вот упрощенный пример кода (который будет в вашем шаблоне страницы — например, page-page-of-posts.php):
/**
* Название шаблона: Страница с записями
*/
while(have_posts()) { // оригинальный основной цикл - контент страницы
the_post();
the_title(); // заголовок страницы
the_content(); // контент страницы
// и т.д...
}
// теперь отображаем список записей нашего пользовательского типа поста
// сначала получаем параметры пагинации
$paged = 1;
if(get_query_var('paged')) {
$paged = get_query_var('paged');
} elseif(get_query_var('page')) {
$paged = get_query_var('page');
}
// запрашиваем записи и заменяем основной запрос (страницы) этим (чтобы пагинация работала)
query_posts(array('post_type' => 'my_post_type', 'post_status' => 'publish', 'paged' => $paged));
// пагинация
next_posts_link();
previous_posts_link();
// цикл
while(have_posts()) {
the_post();
the_title(); // заголовок вашей записи пользовательского типа поста
the_content(); // контент вашей записи пользовательского типа поста
}
wp_reset_query(); // устанавливает основной запрос (глобальная $wp_query) в исходный запрос страницы (он получает его из глобальной переменной $wp_the_query) и сбрасывает данные поста
// Теперь мы можем снова отобразить контент, связанный со страницей (если захотим)
while(have_posts()) { // оригинальный основной цикл - контент страницы
the_post();
the_title(); // заголовок страницы
the_content(); // контент страницы
// и т.д...
}
Теперь, чтобы быть абсолютно ясным, мы могли бы избежать использования query_posts()
здесь и использовать вместо этого WP_Query
— вот так:
// ...
global $wp_query;
$wp_query = new WP_Query(array('ваши параметры запроса здесь')); // устанавливает новый пользовательский запрос как основной
// ваш цикл для пользовательского типа поста здесь
wp_reset_query();
// ...
Но зачем нам это делать, когда у нас есть такая удобная маленькая функция?

Брайан, спасибо за это. Я долго пытался заставить pre_get_posts работать на странице в ТОЧНО таком сценарии, который ты описал: клиенту нужно добавить произвольные поля/контент к тому, что в противном случае было бы архивной страницей, поэтому нужно создать "страницу"; клиенту нужно что-то видеть для добавления в меню навигации, так как добавление произвольной ссылки для них слишком сложно; и т.д. +1 от меня!

Это также можно сделать с помощью "pre_get_posts". Я использовал это для создания "статической главной страницы", которая выводит мои пользовательские типы записей в произвольном порядке и с произвольным фильтром. Эта страница также имеет пагинацию. Посмотрите этот вопрос, чтобы понять, как это работает: http://wordpress.stackexchange.com/questions/30851/how-to-use-a-custom-post-type-archive-as-front-page/30854
Короче говоря, по-прежнему нет более законного сценария для использования query_posts ;)

Потому что "Следует отметить, что использование этой функции для замены основного запроса на странице может увеличить время загрузки страницы, в худших случаях более чем вдвое увеличивая объем необходимой работы или даже больше. Несмотря на простоту использования, функция также склонна вызывать путаницу и проблемы в дальнейшем." Источник http://codex.wordpress.org/Function_Reference/query_posts

Этот ответ совершенно неверен. Вы можете создать "Страницу" в WordPress с таким же URL, как у пользовательского типа записи. Например, если ваш CPT называется Bananas, вы можете создать страницу с именем Bananas и тем же URL. Тогда у вас получится siteurl.com/bananas. Если у вас в папке темы есть файл archive-bananas.php, то он будет использоваться как шаблон и "переопределит" эту страницу. Как указано в одном из других комментариев, использование этого "метода" создает двойную нагрузку на WordPress, поэтому его НИКОГДА не следует использовать.

Я изменяю запрос WordPress из файла functions.php:
//К сожалению, условие "IS_PAGE" не работает в pre_get_posts (это поведение WORDPRESS)
//поэтому вы можете использовать `add_filter('posts_where', ....);` ИЛИ изменить запрос "PAGE" непосредственно в файле шаблона
add_action( 'pre_get_posts', 'myFunction' );
function myFunction($query) {
if ( ! is_admin() && $query->is_main_query() ) {
if ( $query->is_category ) {
$query->set( 'post_type', array( 'post', 'page', 'my_postType' ) );
add_filter( 'posts_where' , 'MyFilterFunction_1' ) && $GLOBALS['call_ok']=1;
}
}
}
function MyFilterFunction_1($where) {
return (empty($GLOBALS['call_ok']) || !($GLOBALS['call_ok']=false) ? $where : $where . " AND ({$GLOBALS['wpdb']->posts}.post_name NOT LIKE 'Journal%')";
}

Просто чтобы обозначить некоторые улучшения к принятому ответу, так как WordPress развивался со временем, и сейчас (спустя пять лет) некоторые вещи изменились:
pre_get_posts
— это фильтр для изменения любого запроса. Чаще всего он используется для изменения только 'основного запроса':
На самом деле это хук действия, а не фильтр, и он влияет на любой запрос.
Основной запрос появляется в ваших шаблонах как:
if( have_posts() ):
while( have_posts() ): the_post();
//Цикл
endwhile;
endif;
На самом деле, это тоже неверно. Функция have_posts
итерирует объект global $wp_query
, который не относится только к основному запросу. global $wp_query;
может быть изменён и второстепенными запросами.
function have_posts() {
global $wp_query;
return $wp_query->have_posts();
}
get_posts()
По сути, это обёртка для отдельного экземпляра объекта WP_Query.
На самом деле, сейчас WP_Query
— это класс, поэтому у нас есть экземпляр класса.
В заключение: во время, когда @StephenHarris писал это, скорее всего, всё было правдой, но со временем в WordPress произошли изменения.

Технически, под капотом всё сводится к фильтрам, действия — это просто простые фильтры. Но вы правы, это действие, которое передаёт аргумент по ссылке, что отличает его от более простых действий.

get_posts
возвращает массив объектов записей, а не объект WP_Query
, так что это действительно по-прежнему верно. И WP_Query
всегда был классом, экземпляр класса = объект.
