ГлавнаяРазноеПочему не следует использовать query_posts() в WordPress

Почему не следует использовать query_posts() в WordPress

Если вы разрабатываете темы и плагины для WordPress, то вы наверняка встречались с функцией query_posts() в многочисленных примерах и уроках в сети. В этой статье мы расскажем почему вам не следует никогда использовать эту функцию.

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

Основной запрос (или основной цикл) в WordPress это тот, который выполняется на раннем этапе загрузки ядра, он строится из запрошенного URL, настроек постоянных ссылок и т.д. Во время основного запроса WordPress определяет такие параметры как количество записей на страницу, используемый шаблон в теме и прочие. Основной запрос делает сам WordPress.

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

  • Вывести в боковой колонке список самых популярных записей
  • Вывести выделенные записи в слайдере на главной странице
  • Вывести записи из той же категории в блоке «похожие записи»

Вторичные запросы выполняются с помощью класса WP_Query, или с помощью одной из вспомогательных функций get_posts(), query_posts() и т.д. Если вы не знакомы с WP_Query, советуем прочитать нашу статью.

Основным отличием query_posts() от других вспомогательных функций является то, что query_posts() заменяет основной цикл на новый, вторичный цикл WordPress. Это значит, что в цикле с query_posts() мы можем работать так же, как и в обычном основном цикле.

Для сравнения рассмотрим цикл для вывода популярных записей с помощью WP_Query:

// Вторичный цикл
$popular = new WP_Query( ... );
while ( $popular->have_posts() ) {
    $popular->the_post();

    the_title(); // вывести название
    the_content(); // вывести содержимое
}

Этот же цикл с помощью query_posts():

// Вторичный цикл
query_posts( ... );
while ( have_posts() ) {
    the_post();

    the_title(); // вывести название
    the_content(); // вывести содержимое
}

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

Именно поэтому разработчики часто думают, что query_posts() изменяет основной запрос WordPress, но это не так. Функция query_posts() заменяет основной цикл новым вторичным циклом, и происходит это после выполнения основного запроса.

$wp_query и $wp_the_query

После выполнения основного запроса WordPress помещает его результат в глобальную переменную $wp_the_query, а в глобальной переменной $wp_query хранится всего лишь ссылка на $wp_the_query. Функции have_posts(), the_post() и прочие работают именно с глобальным объектом $wp_query.

Чтобы в этом убедиться достаточно взглянуть на реализацию подобных функций:

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

Функция query_posts() создает новый вторичный запрос с помощью WP_Query и помещает результат в эту же глобальную переменную $wp_query:

function query_posts( $query ) {
    $GLOBALS['wp_query'] = new WP_Query();
    return $GLOBALS['wp_query']->query( $query );
}

Таким образом функции, которые предназначены для работы с основным циклом WordPress начинают работать с нашим вторичным запросом, а основной запрос остался в глобальной переменной $wp_the_query, ссылку на которую можно восстановить с помощью функции wp_reset_query().

function wp_reset_query() {
    $GLOBALS['wp_query'] = $GLOBALS['wp_the_query'];
    wp_reset_postdata();
}

После этого, функции have_posts() и другие вновь работают с основным циклом WordPress, но разработчики часто об этом забывают, в результате чего перестает работать пагинация, некоторые виджеты и прочее.

Пагинация

Самым частым результатом использования query_posts() является сломанная пагинация, когда например первые две страницы работают, а третья и четвертая возвращают ошибку 404. Давайте рассмотрим как, и почему это происходит.

По умолчанию WordPress показывает десять записей на одной странице. Допустим у нас всего двадцать записей, это всего две страницы. Изменить количество записей на страницу можно легко с помощью query_posts() в начале нашего шаблона index.php или archive.php:

global $query_string;
query_posts( $query_string . '&posts_per_page=5' );

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

Напоминаем, что основной запрос WordPress происходит еще до того, как обрабатываются шаблоны index.php или archive.php, где происходит наша «подмена». В основном запросе количество записей на страницу — десять, и всего две страницы. Третей и четвертой страниц в основном запросе нет.

Именно основной запрос определяет какой шаблон темы будет использоваться, и при запросе третей или четвертой страницы WordPress будет использовать шаблон 404.php.

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

Событие pre_get_posts

Наиболее правильным способом изменить основной цикл WordPress является событие pre_get_posts, которое происходит перед каждым запросом WP_Query. Работать с этим событием можно в плагине или в файле functions.php вашей темы:

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

Важно отметить, что pre_get_posts вызывается для каждого запроса WP_Query, включая основной запрос, навигационное меню, вторичные запросы в виджетах и прочее. С помощью метода is_main_query() мы изменяем параметры только основного запроса, а с помощью проверки is_admin() мы меняем запрос только на лицевой части сайта и не в административной панели.

Результат такого подхода тот же, что и с query_posts(), но в этот раз будет работать пагинация, а загрузка страницы будет происходить быстрее, поскольку с pre_get_posts мы действительно изменили основной запрос перед его выполнением и не выполняли вторичных запросов.

Альтернативы query_posts()

Если вам необходимо выполнить вторичный запрос в WordPress, воспользуйтесь функцией get_posts() или конструкцией new WP_Query(). При работе с ними ваш код будет более явным и понятным для читающих.

Когда вам необходимо изменить основной запрос WordPress перед его выполнением, самым простым способом является событие pre_get_posts, или фильтр request, который выполняется еще раньше, чем pre_get_posts и только для основного запроса.

Если у вас возникли вопросы про query_posts() или WP_Query, оставьте комментарий и мы обязательно вам ответим.

Подписаться на рассылку

Подписаться → Подпишитесь на бесплатную рассылку журнала WP Magazine и получайте новости, события, подборки тем и плагинов, уроки, советы и многое другое в мире WordPress!

  • SeVlad

    Полезнейшая статья! Спасибо, Константин.

  • Владимир Петрозаводский

    Ну и почему нельзя использовать, можно же, главное не переопределять же использовать то можно ещё как

    • Использовать-то можно, но не следует. Любой вызов query_posts() переопределяет основной цикл своим собственным, плюсов в этом никаких. Лучше использовать явные get_posts() и new WP_Query(), что в результате делает то же самое, только без этих «хитрых» переопределений.

      • Mikel Izek

        Используя get_posts() как я могу использовать фильтры которые применяются для «the_content», например. Удобно ли это? И разве это проблема после каждого вторичного запроса использовать wp_reset_query(), и при необходимости хуки — pre_get_posts и request ?

        • С get_posts() вам придется использовать setup_postdata() для заполнения глобальных переменных и использования the_content() и др., а после зарвешения wp_reset_postdata() для возврата в основной цикл. В запросах с помощью new WP_Query() или query_posts() это делает метод the_post(), но по завершению лучше также возвращать переменные основного цикла с помощью wp_reset_postdata() (плюс wp_reset_query() если вы используете query_posts()).

  • Александр

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

    • Если вы хотите сделать пагинацию для отдельного блока, не связанного с основным циклом WordPress, лучше всего использовать дополнительный query_var, например my_widget_paged. Зарегистрировать его можно с помощью фильтра query_vars, а обрабатывать в вашем запросе к get_posts(). Таким образом пагинаци в виджете у вас будет реализована с помощью дополнительной независимой переменной $_GET: /?my_widget_paged=2 и т.д.

      • Александр

        Каким образом мне узнать общее количество записей? Т.е. с помощью $_GET: /?my_widget_paged=2 я могу узнать страницу, могу рассчитать параметр offset, но я не смогу посчитать сколько всего страниц возможно, не зная общего кол-ва записей. get_posts не возвращает этого параметра.

        • Параметр offset вам не нужен, можно воспользоваться параметром paged, а вот узнать общее количество найденых записей к сожалению с get_posts() не получится. Вам нужен объект WP_Query и его свойство found_posts. Если вам удобнее работать с массивом, после выполнения запроса с WP_Query вы можете обратиться к $query->posts, это то же самое что и функция get_posts().

  • misak

    Добрый день! Отличный блог! Вижу вы профи и хочу спросить: есть блог с обычной страницей записей, хотел бы добавить еще 1 страницу записей, что бы на одну выводились записи из 1 рубрики, а на вторую из рубрики 2! Как можно это осуществить?

    • Вы можете воспользоваться архивами рубрик и даже вывести их в меню. В настройках постоянных ссылок вы можете установить / в качестве префикса для рубрик.

      Если же интересует реализация с помощью кода, то вам придется создать новый query_var, читать его в событии pre_get_posts и изменять запрос соответствующе. С помощью фильтра template_include и функции locate_template() вы можете указать какой именно шаблон будет использоваться для отображения содержимого.

  • Thorny

    Заметил такую ошибку при работе с pre_get_posts
    при переопределении «post_type» из строки в массив вываливается ошибка :(

    $query->set( ‘post_type’, array(‘slag’) ); — > Warning: Illegal offset type in isset or empty in Z:homeartilkomwwwwp-includespost.php on line 1060

    $query->set( ‘post_type’, ‘slag’ ); — > Все ок, без ошибок

    Есть у кого мысли по этому поводу???

    • Вопрос решён на форуме. Проблема была в плагине WordPress SEO (#948), исправлена в версии 1.5.2.6.

      • Thorny

        Спасибо Вам еще раз )) теперь тут.

  • Rusty

    Как вывести записи по определенному тегу не используя query_posts?

    Если будет несложно, напишите полный цикл. Большое спасибо :)

    • Rusty, смотря в каком контексте вы хотите это вывести. Если это отдельный блок, то так же как и с query_posts() только через новый объект WP_Query. Если вы хотите изменить основной цикл, то через событие pre_get_posts. Оба метода описаны в статье.

  • CVC

    У меня, проблема при нажатие на 3 страницу в пагинации, в командной строке появляется

    ?page=2?page=3, хотя если набрать руками ?page=2 и ?page=3 работают но в пагинаци можно нажать только один раз а потом он дописывает уже к имеюшейся ссылке.

    в шаблоне пагинация реализованна вот так:

    max_num_pages;

    if ($total_pages > 1){

    $current_page = max(1, get_query_var(‘paged’));

    echo paginate_links(array(

    ‘base’ => get_pagenum_link(1) . ‘%_%’,

    ‘format’ => ‘?page=%#%’,

    ‘current’ => $current_page,

    ‘total’ => $total_pages,

    ));

    }

    ?>

    • CVC

      в шаблоне пагинация реализованна вот так:
      ВЫШЕ НЕ ВСЕ
      global $wp_query;
      $total_pages = $wp_query->max_num_pages;
      if ($total_pages > 1){
      $current_page = max(1, get_query_var(‘paged’));
      echo paginate_links(array(
      ‘base’ => get_pagenum_link(1) . ‘%_%’,
      ‘format’ => ‘?page=%#%’,
      ‘current’ => $current_page,
      ‘total’ => $total_pages,

      ));

      }

  • Сергей

    Помогите с такой проблемой, пробовал разный способы но не получается, поставить на главную страницу случайную запись из любой категорий притом чтоб показывалась вся запись как правильна создать pre_get_posts и куда точней вставить в шаблон function.php и на страницу где будет случайную запись что вставить я ставлю вот так но ноль на массу Спасибо.

    • Сергей, если вам нужна случайная запись в добавок к существующим последним записям, лучше всего организовать второй цикл с помощью WP_Query. Если же вам нужно заменить имеющийся цикл и выводить лишь одну запись случайным образом, тогда параметр orderby в значение rand внутри pre_get_posts.

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

  • Она предназначалась как альтернатива вторичному циклу с WP_Query, и в ней есть дополнительное «удобство» работы с функциями have_posts(), the_post() и т.д. без указания определенного объекта WP_Query. Но за этим «удобством» скрывается хитрая магия внутри query_posts(), из-за которой ее использовать не стоит.

  • Atapys

    ого, не знал, про query_posts
    очень полезная статья, пошёл оптимизировать свой код

  • А что если мне нужно создать файл шаблона, например my-page.php и в нем изменить основной запрос? Как это сделать?

    • Если у вас файл шаблона (page template) то основной запрос у вас будет на ту страницу, которая использует этот шаблон. Вы не можете изменить этот основной запрос, иначе WordPress не подключит ваш файл шаблона.

      Внутри самого шаблона вы можете использовать new WP_Query(); для запуска вторичного запроса.

      • Я немного не правильно выразился, я имел ввиду изменить основной запрос с помощью pre_get_posts. Мне подсказали что можно использовать is_page(id), для конкретной страницы, чтобы изменить основной запрос. Все правильно?

        • Если вы хотите вместо запрашиваемой конкретной страницы вывести что-то другое, то да, в pre_get_posts вы можете посмотреть $query->is_page(), но делать вы это будете не в файле шаблона для этой страницы, а раньше, например в functions.php или внутри плагина, т.к. изменив запрос WordPress ваш шаблон my-page.php больше использовать не будет.

          • Скажите пожалуйста, прочитал что is_page($id) нельзя использовать в pre_.. Как решить это если мне нужно указать конкретную страницу?

  • Евгений

    Так и не понял, почему автор против работы с query_posts() ? Все функции выполняются последовательно, главное не забывать сбрасывать через wp_reset_query(). Пагинация не будет работать таким образом, как вы описали, и при использовании get_posts() и new WP_Query(). Если пагинацию менять через плагин, то можно и переменные новые задать в URL через add_rewrite_tag() и add_rewrite_rule(). Либо произвольные параметры в get Запрос, например /news/politika?p_page=2 и можно пользовать любые способы вывода.

    • Евгений, родная пагинация будет работать если использовать событие pre_get_posts для работы с основными запросами.

      Пагинация будет также «работать» если использовать get_posts() или new WP_Query(), но она будет основана на главном запросе, т.е. если вы используете шаблон страницы, то у вас будет всего одна страница, не зависимо от количества результатов и страниц во вторичных запросах.

      А вот с query_posts() это поведение становится неявным, так как пагинация будет основана на результатах вторичного запроса, с обрабатывать основной, т.е. пока вы не наткнетесь на 404, о сломанной пагинации можно так и не узнать.

  • Сергеевич

    Здраствуйте! У меня вопрос как сделать
    рабочию пагинацию wp_pagenavi в коде (ниже) . А то сортировка происходит
    только на одной странице, при переходе на другую страницу сортировка
    сбивается.

    <option value="title">
    <option value="newest">
    <option value="oldest">
    <option value="rating">По рейтингу
    <option value="komment">По комментариям

  • Сергеевич
  • Лев

    Как Вы думаете, get_posts в виджетах — это хорошая практика, или лучше использовать нечто другое?

    • Хорошая. Если вы используете setup_postdata() не забудьте по завершению вызвать wp_reset_postdata().

  • Лев

    Здравствуйте.
    у меня есть шорткод для вывода информации из одного поста в другой
    вот фрагмент

    global $post;
    $showargs = array(‘post_type’=>’ads’,’order’=>’DESC’,’post__in’=>explode(‘,’,$ids),’orderby’=>’post__in’,’posts_per_page’=>-1 );
    $showloop = new WP_Query( $showargs );
    while ( $showloop->have_posts() ) : $showloop->the_post();
    $out = get_the_excerpt();
    $reout .= apply_filters(‘the_content’, $out);
    endwhile;
    wp_reset_query();

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

    Сейчас попытаюсь объяснить. Допустим, мне надо вывести посты, связанные с текущим постом. Если я использую шорткод, то эти данные не будут отображаться, т.к. у постов из типа данных ads нет тегов и категорий, а мне надо, чтобы они были связаны с тем, в который включен шорткод.

    Это делается при помощи query var ? Если да, то как лучше сделать?

  • Владимир

    Доброго времени суток!
    Вопрос, возможно, не по теме-извиняюсь!
    Вопрос такой: Сверстаны 2 шаблона. Имеется необходимость вывода даты
    поста и автора только в новостях. Создан файл archive.php с небольшим
    отличаем от индексного(в индексном нет автора и даты публикации + нет
    постраничной навигации). Редактор в админ-панели видит файл archive.php с его
    содержимым, но при переходе на страницу новостей отображение происходит
    на основе индексного. Не могу понять в чем проблема.

    З.ы.: на локалке (Денвер) все отлично с обоими шаблонами. На локалке вордпресс 4.0. на
    рабочем 4.1.1 (но имеет ли это значение).

    • Владимир

      Причина найдена………

  • Николай

    Я вижу многие испугались ваших фраз: «переопределяет основной цикл своим собственным данными», «хитрые переопределения».

    Вы меня не убедили, что query_posts — это плохо. Это нормально, как и другие функции.
    Меньше запросов к базе? Используется меньший объем памяти? Ответ: вряд ли!

    Единственное, не стоит ее сувать куда попало. Проблемы могут возникнуть используя query_posts цикл в цикле, и то, только если есть непосредственное взаимодействие данных обеих циклов (как например, пагинация — про которую вы говорили в статье). В остальном — ложные обвинения.

    • Николай

      Хотя, вы знаете, я бы тоже говорил новичкам и малоопытным разработчикам, что лучше не использовать эту функцию. Нужно уметь грамотно пользоваться этой функцией, знать — когда можно, когда нет. Новичка, легче научить пользоваться get_posts(), чем объяснять нюансы использования query_posts(). Если пост написан из этих соображений, я с вами солидарен!

  • Даимир

    Кама тоже пишет, что лучше юзать get_posts() вместо query_posts()