Исключение или включение ID категорий в WP_Query
У меня есть WordPress-сайт с более чем 300 категориями.
Теперь появилось требование дать пользователям возможность выбирать категории. Изначально я сделал так, чтобы все категории были предварительно выбраны, и если нужно исключить категорию, её можно снять выделение.
Проблема, с которой я столкнулся, заключается в том, как правильно выводить результаты в соответствии с выбранными категориями.
Мой первый подход был просто исключить все снятые категории, как показано ниже:
Например: исключить категории 10, 11, 12
$args = array(
'category__not_in' => array('10','11','12')
);
Допустим, у меня есть запись, которая относится к категориям 12 и 13
. Согласно приведённому выше коду, я не получу эту запись в результатах, так как исключаются записи из категории 12
. Но по идее она должна быть в результатах, потому что категория 13
не была снята.
В качестве решения я мог бы использовать параметр 'category__in'
со всеми выбранными ID категорий. Но меня беспокоит, что список будет очень длинным, даже если он формируется программно, и я не уверен в нагрузке на wp_query
, учитывая что у меня более 300 категорий.
У кого-нибудь есть идеи, как лучше решить эту проблему?
Как вы, вероятно, знаете, категории являются таксономиями. Когда вы используете аргументы типа category__in
, это добавит таксономический запрос в ваш WP_Query()
. Таким образом, ваша ситуация будет выглядеть примерно так:
$args = array(
'post_type' => 'post',
'tax_query' => array(
'relation' => 'AND',
array(
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => array( 12 ),
'operator' => 'IN',
),
array(
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => array( 11, 12, 13 ),
'operator' => 'NOT IN',
),
),
);
$query = new WP_Query( $args );
Я бы не стал беспокоиться о проблемах с производительностью в данном случае. Это, скорее всего, ваше единственное решение, если вы не хотите напрямую запрашивать записи из базы данных с помощью SQL-запроса (это может немного улучшить производительность).

Но разве у нас не будет той же самой проблемы? Автор хотел использовать что-то, чтобы избежать перечисления всех категорий, но в вашем случае придется перечислять категории внутри части IN, верно?

category__in
или category__and
— это то, что вам нужно, как сказал Jack. Если вы хотите ускорить свои запросы, возможно, стоит рассмотреть использование transient-ов.

Также ответ graziondev ниже тоже верен, нет необходимости использовать category__in
при первоначальном формировании запроса, так как вы по умолчанию включаете все категории. Загрузите этот запрос в transient, а затем фильтруйте последующие запросы из этих данных, если вам нужна скорость.

Предположим, у нас есть 4 записи и 4 категории.
+----+--------+
| ID | Запись |
+----+--------+
| 1 | Тест 1 |
| 2 | Тест 2 |
| 3 | Тест 3 |
| 4 | Тест 4 |
+----+--------+
+----+------------+
| ID | Категория |
+----+------------+
| 1 | Категория 1 |
| 2 | Категория 2 |
| 3 | Категория 3 |
| 4 | Категория 4 |
+----+------------+
+--------+------------------------+
| Запись | Категория |
+--------+------------------------+
| Тест 1 | Категория 1, Категория 2 |
| Тест 2 | Категория 2 |
| Тест 3 | Категория 3 |
| Тест 4 | Категория 4 |
+--------+------------------------+
Если я правильно понял ваш вопрос, вы хотите получить запись Тест 1
, используя параметр category__not_in
. Аргументы вашего запроса будут выглядеть так:
$args = array(
'category__not_in' => array(2, 3, 4)
);
Проблема с category__not_in
заключается в том, что он генерирует SQL-запрос с NOT IN SELECT
.
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts
WHERE 1=1
AND (wp_posts.ID NOT IN
( SELECT object_id
FROM wp_term_relationships
WHERE term_taxonomy_id IN (2, 3, 4) ))
AND wp_posts.post_type = 'post'
AND (wp_posts.post_status = 'publish'
OR wp_posts.post_status = 'private')
GROUP BY wp_posts.ID
ORDER BY wp_posts.post_date DESC LIMIT 0, 10
NOT IN SELECT
исключит все записи, включая Тест 1
. Если бы этот SQL использовал JOIN
вместо NOT IN SELECT
, это сработало бы.
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts
LEFT JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id)
WHERE 1=1
AND (wp_term_relationships.term_taxonomy_id NOT IN (2, 3, 4))
AND wp_posts.post_type = 'post'
AND (wp_posts.post_status = 'publish'
OR wp_posts.post_status = 'private')
GROUP BY wp_posts.ID
ORDER BY wp_posts.post_date DESC LIMIT 0, 10
Приведенный выше SQL вернет только запись Тест 1
. Мы можем сделать небольшой трюк, чтобы создать такой запрос с использованием класса WP_Query. Вместо использования параметра category__not_in
заменим его на параметр category__in
и добавим фильтр post_where
, который напрямую изменит SQL для наших целей.
function wp_286618_get_posts() {
$query = new WP_Query( array(
'post_type' => 'post',
'category__in' => array( 2, 3, 4 ) // Используем `category__in`, чтобы заставить SQL-запрос использовать JOIN.
) );
return $query->get_posts();
}
function wp_286618_replace_in_operator($where, $object) {
$search = 'term_taxonomy_id IN'; // Ищем оператор IN, созданный параметром `category__in`.
$replace = 'term_taxonomy_id NOT IN'; // Заменяем IN на NOT IN
$where = str_replace($search, $replace, $where);
return $where;
}
add_filter( 'posts_where', 'wp_286618_replace_in_operator', 10, 2 ); // Добавляем фильтр для замены оператора IN
$posts = wp_286618_get_posts(); // Вернет только запись Тест 1
remove_filter( 'posts_where', 'wp_286618_replace_in_operator', 10, 2 ); // Удаляем фильтр, чтобы он не влиял на другие запросы
Преимущество этого решения перед другими заключается в том, что мне не нужно знать ID других категорий, и оно сохранит ваш цикл записей чистым.

Но для меня это не вариант. Обычно люди склонны выбирать почти все категории, но некоторым не нравится видеть лишние посты, поэтому в таком случае они могут отменить выбор категорий. Но по моим предположениям, более 90% категорий будут выбраны. Именно поэтому мы предлагаем отмену выбора, а не выбор.

Не уверен, но думаю, что это нельзя сделать с помощью стандартного поведения/опций WP_Query
. Возможно, обходным решением будет реализация функции, которая выполнит эту проверку после выбора всех записей.
Конечно, недостаток этого метода в том, что сначала нужно выбрать все записи, а затем отфильтровать их, но это может быть решением вашей проблемы. Вот как это можно сделать:
<?php
function test_categorie($cats,$ex_cats) {
$test = false; //считаем, что запись исключена, пока не найдется категория, не входящая в массив
foreach ($cats as $cat){
if(!in_array(intval($cat->term_id),$ex_cats)) {
$test = true;
//Можно прервать цикл, так как у записи есть хотя бы одна неисключенная категория
break;
}
}
return $test;
}
//Определяем исключаемые категории здесь
$exclude_cat = array(11,12,13);
$args = array(
'posts_per_page' => -1,
'post_type' => 'post',
);
$the_query = new WP_Query($args );
if ( $the_query->have_posts() ) :
while ( $the_query->have_posts() ) : $the_query->the_post();
//выполняем проверку, если (false) - пропускаем запись и переходим к следующей
if(!test_categorie(get_the_category(),$exclude_cat))
continue;
/*
Ваш код здесь
*/
endwhile;
wp_reset_postdata();
endif;
?>

Этот ответ сработает, я думаю, хотя у вас могут возникнуть некоторые проблемы, если использование или вывод зависят от корректности запроса в отношении исключения — например, для точной пагинации: вы можете получить страницы, которые должны показывать, скажем, 5 элементов, но вместо этого покажут 2. Конечно, есть обходные пути и для этого, но всегда будут компромиссы того или иного рода.

Попробуйте этот вариант. Думаю, это немного "грязно", но у меня отлично работает!
$skills = get_user_meta($curr_id , 'designer_skills');
$skills = array_map('intval', explode(',' , $skills[0]));
$args = array(
'numberposts' => -1,
'post_type' => 'project',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'status',
'compare' => '=',
'value' => 'open'
),
array(
'key' => 'payment_status',
'compare' => '=',
'value' => true
)
)
);
$posts = get_posts( $args );
if ($posts) {
$count = 0;
foreach ($posts as $project) {
if (in_array(get_the_terms( $project->ID, 'projects')[0] -> term_id , $skills)){
//реализуйте здесь свой собственный код
}
}
}
