Как объединить два запроса в WordPress

16 янв. 2014 г., 17:12:41
Просмотры: 14.4K
Голосов: 14

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

У меня есть следующий код:

<?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...

Как можно исправить эту ошибку?

0
Все ответы на вопрос 4
0

Одиночный запрос

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

Предварительные условия

Сначала необходимо установить (как показано в моем другом ответе) нужные значения внутри фильтра 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, поэтому нам не нужно настраивать все вручную. Код хорошо прокомментирован, но вот несколько примечаний:

  1. Метод accept() позволяет определить критерии, которые разрешают или запрещают обработку элемента в цикле.
  2. Внутри этого метода мы используем WP_Query::the_post(), так что вы можете просто использовать любые теги шаблонов в цикле вашего файла шаблона.
  3. Мы также отслеживаем цикл и перематываем записи, когда достигаем последнего элемента. Это позволяет проходить через бесконечное количество циклов без сброса запроса.
  4. Есть один пользовательский метод, который не является частью спецификации 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() в конце на то, что вы хотите видеть и где вы хотите это видеть. Это всего лишь доказательство концепции.

17 янв. 2014 г. 00:45:31
0

Хотя это не лучший способ решения данной проблемы (ответ @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 может самостоятельно обработать необходимую логику.

16 янв. 2014 г. 18:01:20
2

Вам фактически нужен третий запрос, чтобы получить все записи сразу. Затем вам следует изменить первые два запроса так, чтобы они возвращали не сами записи, а только их 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 с вашими записями.

16 янв. 2014 г. 21:30:38
Комментарии

Спасибо. Это оказалось наименее сложным решением для сохранения одной структуры цикла и несложных расчетов пагинации.

Jay Neely Jay Neely
7 мар. 2016 г. 19:46:29

Это выглядит как простое и более универсальное решение. +1 решение от самого мистера WordPress.

samjco-com samjco-com
19 янв. 2021 г. 08:49:43
0

Фактически, существует 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, напрямую модифицировав запрос для сортировки по мета-значению. Ознакомьтесь с этим ответом, так как текущий пример — лишь отправная точка.

16 янв. 2014 г. 18:00:53