Как объединить два запроса в WordPress
Я пытаюсь упорядочить записи в категории, показывая сначала посты с изображениями, а затем посты без изображений. Мне удалось сделать это с помощью двух отдельных запросов, но теперь я хочу объединить эти два запроса в один.
У меня есть следующий код:
<?php
// Запрос для постов с миниатюрами
$loop = new WP_Query( array('meta_key' => '_thumbnail_id', 'cat' => 1 ) );
// Запрос для постов без миниатюр
$loop2 = new WP_Query( array('meta_key' => '', 'cat' => 1 ) );
// Попытка объединить запросы
$mergedloops = array_merge($loop, $loop2);
while($mergedloops->have_posts()): $mergedloops->the_post(); ?>
Но при попытке просмотреть страницу я получаю следующую ошибку:
Fatal error: Call to a member function have_posts() on a non-object in...
Затем я попробовал преобразовать результат array_merge в объект, но получил другую ошибку:
Fatal error: Call to undefined method stdClass::have_posts() in...
Как можно исправить эту ошибку?

Одиночный запрос
Подумав об этом немного больше, есть вероятность, что можно обойтись одним/основным запросом. Иными словами: нет необходимости в двух дополнительных запросах, если можно работать с запросом по умолчанию. А если нельзя работать с запросом по умолчанию, вам не понадобится больше одного запроса, независимо от того, на сколько циклов вы хотите разделить запрос.
Предварительные условия
Сначала необходимо установить (как показано в моем другом ответе) нужные значения внутри фильтра pre_get_posts
. Там вы, скорее всего, установите posts_per_page
и cat
. Пример без фильтра pre_get_posts
:
$catID = 1;
$catQuery = new WP_Query( array(
'posts_per_page' => -1,
'cat' => $catID,
) );
// Добавляем заголовок:
printf( '<h1>%s</h1>', number_format_i18n( $catQuery->found_posts )
.__( " Записи в рубрике ", 'YourTextdomain' )
.get_cat_name( $catID ) );
Создание основы
Следующее, что нам нужно — это небольшой пользовательский плагин (или просто поместить его в файл functions.php
, если вас не беспокоит его перемещение во время обновлений или изменений темы):
<?php
/**
* Plugin Name: (#130009) Объединение двух запросов
* Description: "Объединяет" два запроса, используя <code>RecursiveFilterIterator</code> для разделения одного основного запроса на два запроса
* Plugin URl: http://wordpress.stackexchange.com/questions/130009/how-to-merge-two-queries-together
*/
class ThumbnailFilter extends FilterIterator implements Countable
{
private $wp_query;
private $allowed;
private $counter = 0;
public function __construct( Iterator $iterator, WP_Query $wp_query )
{
NULL === $this->wp_query AND $this->wp_query = $wp_query;
// Экономим время обработки, сохранив его один раз
NULL === $this->allowed
AND $this->allowed = $this->wp_query->have_posts();
parent::__construct( $iterator );
}
public function accept()
{
if (
! $this->allowed
OR ! $this->current() instanceof WP_Post
)
return FALSE;
// Переключаем индекс, устанавливаем данные записи и т.д.
$this->wp_query->the_post();
// Достигнута последняя запись WP_Post: Настраиваем WP_Query для следующего цикла
$this->wp_query->current_post === $this->wp_query->query_vars['posts_per_page'] -1
AND $this->wp_query->rewind_posts();
// Не соответствует критериям? Прерываем.
if ( $this->deny() )
return FALSE;
$this->counter++;
return TRUE;
}
public function deny()
{
return ! has_post_thumbnail( $this->current()->ID );
}
public function count()
{
return $this->counter;
}
}
Этот плагин делает одну вещь: он использует SPL (Стандартную библиотеку PHP) и ее интерфейсы и итераторы. Теперь у нас есть FilterIterator
, который позволяет удобно удалять элементы из нашего цикла. Он расширяет PHP SPL Filter Iterator, поэтому нам не нужно настраивать все вручную. Код хорошо прокомментирован, но вот несколько примечаний:
- Метод
accept()
позволяет определить критерии, которые разрешают или запрещают обработку элемента в цикле. - Внутри этого метода мы используем
WP_Query::the_post()
, так что вы можете просто использовать любые теги шаблонов в цикле вашего файла шаблона. - Мы также отслеживаем цикл и перематываем записи, когда достигаем последнего элемента. Это позволяет проходить через бесконечное количество циклов без сброса запроса.
- Есть один пользовательский метод, который не является частью спецификации
FilterIterator
:deny()
. Этот метод особенно удобен, так как содержит только наше условие "обрабатывать или нет", и мы можем легко переопределить его в последующих классах, не зная ничего, кроме тегов шаблонов WordPress.
Как организовать цикл?
С этим новым итератором нам больше не нужны if ( $customQuery->have_posts() )
и while ( $customQuery->have_posts() )
. Мы можем использовать простое выражение foreach
, так как все необходимые проверки уже сделаны за нас. Пример:
global $wp_query;
// Сначала нам нужен ArrayObject из фактических записей
$arrayObj = new ArrayObject( $wp_query->get_posts() );
// Затем мы передаем его в наш новый пользовательский фильтр-итератор
// Мы передаем объект $wp_query вторым аргументом, чтобы отслеживать его
$primaryQuery = new ThumbnailFilter( $arrayObj->getIterator(), $wp_query );
В конечном итоге нам не нужно ничего, кроме стандартного цикла foreach
. Мы даже можем опустить the_post()
и все равно использовать все теги шаблонов. Глобальный объект $post
всегда будет синхронизирован.
foreach ( $primaryQuery as $post )
{
var_dump( get_the_ID() );
}
Дополнительные циклы
Теперь приятная особенность заключается в том, что каждый последующий фильтр запросов довольно прост в обработке: просто определите метод deny()
, и вы готовы к следующему циклу. $this->current()
всегда будет указывать на текущую запись в цикле.
class NoThumbnailFilter extends ThumbnailFilter
{
public function deny()
{
return has_post_thumbnail( $this->current()->ID );
}
}
Поскольку мы определили, что теперь deny()
запрещает обработку каждой записи, у которой есть миниатюра, мы можем сразу перебирать все записи без миниатюр:
foreach ( $secondaryQuery as $post )
{
var_dump( get_the_title( get_the_ID() ) );
}
Протестируйте.
Следующий тестовый плагин доступен как Gist на GitHub. Просто загрузите и активируйте его. Он выводит/дамп ID каждой записи в цикле как callback для действия loop_start
. Это означает, что в зависимости от вашей настройки, количества записей и конфигурации может быть довольно много вывода. Пожалуйста, добавьте некоторые условия прерывания и измените var_dump()
в конце на то, что вы хотите видеть и где вы хотите это видеть. Это всего лишь доказательство концепции.

Хотя это не лучший способ решения данной проблемы (ответ @kaiser предпочтительнее), чтобы напрямую ответить на вопрос, фактические результаты запроса будут находиться в $loop->posts
и $loop2->posts
, так что...
$mergedloops = array_merge($loop->posts, $loop2->posts);
...должно сработать, но вам придётся использовать цикл foreach
, а не стандартную структуру цикла на основе WP_Query
, так как слияние запросов таким образом нарушит "мета"-данные объекта WP_Query
о цикле.
Также можно сделать так:
$loop = new WP_Query( array('fields' => 'ids','meta_key' => '_thumbnail_id', 'cat' => 1 ) );
$loop2 = new WP_Query( array('fields' => 'ids','meta_key' => '', 'cat' => 1 ) );
$ids = array_merge($loop->posts, $loop2->posts);
$merged = new WP_Query(array('post__in' => $ids,'orderby' => 'post__in'));
Конечно, эти решения предполагают множественные запросы, поэтому подход @Kaiser предпочтительнее для подобных случаев, где WP_Query
может самостоятельно обработать необходимую логику.

Вам фактически нужен третий запрос, чтобы получить все записи сразу. Затем вам следует изменить первые два запроса так, чтобы они возвращали не сами записи, а только их ID в удобном для работы формате.
Параметр 'fields'=>'ids'
заставит запрос вернуть массив ID номеров подходящих записей. Но нам не нужен весь объект запроса, поэтому используем get_posts для этих целей.
Сначала получим ID записей, которые нам нужны:
$imageposts = get_posts( array('fields'=>'ids', 'meta_key' => '_thumbnail_id', 'cat' => 1 ) );
$nonimageposts = get_posts( array('fields'=>'ids', 'meta_key' => '', 'cat' => 1 ) );
Теперь $imageposts и $nonimageposts содержат массивы ID номеров записей, которые мы объединяем:
$mypostids = array_merge( $imageposts, $nonimageposts );
Удаляем дублирующиеся ID...
$mypostids = array_unique( $mypostids );
Теперь создаем запрос для получения самих записей в указанном порядке:
$loop = new WP_Query( array('post__in' => $mypostids, 'ignore_sticky_posts' => true, 'orderby' => 'post__in' ) );
Переменная $loop теперь содержит объект WP_Query с вашими записями.

Фактически, существует meta_query
(или WP_Meta_Query
) — он принимает массив массивов, где можно искать строки _thumbnail_id
. Если затем проверить на EXISTS
, можно получить только те записи, у которых есть это поле. Объединив это с аргументом cat
, вы получите только записи, которые относятся к категории с ID 1
и имеют миниатюру. Если затем отсортировать их по meta_value_num
, то они будут упорядочены по ID миниатюры от меньшего к большему (как указано в order
и ASC
). При использовании EXISTS
в качестве значения compare
не требуется указывать value
.
$thumbsUp = new WP_Query( array(
'cat' => 1,
'meta_query' => array(
array(
'key' => '_thumbnail_id',
'compare' => 'EXISTS',
),
),
'orderby' => 'meta_value_num',
'order' => 'ASC',
) );
Теперь при переборе записей вы можете собрать все ID и использовать их в исключающем условии для дополнительного запроса:
$postsWithThumbnails = array();
if ( $thumbsUp->have_posts() )
{
while ( $thumbsUp->have_posts() )
{
$thumbsUp->the_post();
// Собираем ID
$postsWithThumbnails[] = get_the_ID();
// Здесь можно выполнять отображение или другие действия
}
}
Теперь можно добавить второй запрос. Здесь не нужно использовать wp_reset_postdata()
, так как всё хранится в переменной, а не в основном запросе.
$noThumbnails = new WP_Query( array(
'cat' => 1,
'post__not_in' => $postsWithThumbnails
) );
// Перебор записей без миниатюр
Конечно, можно поступить умнее и изменить SQL-запрос внутри pre_get_posts
, чтобы не тратить основной запрос. Также можно выполнить первый запрос ($thumbsUp
выше) внутри фильтра pre_get_posts
.
add_filter( 'pre_get_posts', 'wpse130009excludeThumbsPosts' );
function wpse130009excludeThumbsPosts( $query )
{
if ( $query->is_admin() )
return $query;
if ( ! $query->is_main_query() )
return $query;
if ( 'post' !== $query->get( 'post_type' ) )
return $query;
// Нужно только если запрос для архива категории с ID 1
if (
$query->is_archive()
AND ! $query->is_category( 1 )
)
return $query;
$query->set( 'meta_query', array(
array(
'key' => '_thumbnail_id',
'compare' => 'EXISTS',
),
) );
$query->set( 'orderby', 'meta_value_num' );
// Если мы не на странице архива категории с ID = 1, добавляем:
$query->set( 'category__in', 1 );
return $query;
}
Это изменяет основной запрос, поэтому мы получим только записи с миниатюрами. Теперь можно (как показано в первом запросе выше) собрать ID в основном цикле, а затем добавить второй запрос для отображения остальных записей (без миниатюр).
Кроме того, можно пойти ещё дальше и изменить posts_clauses
, напрямую модифицировав запрос для сортировки по мета-значению. Ознакомьтесь с этим ответом, так как текущий пример — лишь отправная точка.
