Cómo excluir o incluir IDs de categorías en WP_Query de WordPress
Tengo una instalación de WordPress con más de 300 categorías.
Necesito dar flexibilidad para seleccionar categorías. Inicialmente marqué todas las categorías como seleccionadas, y si alguien necesita excluir una categoría, puede deseleccionarla.
El problema es cómo mostrar resultados precisos según la selección de categorías.
Mi primer enfoque fue excluir todas las categorías deseleccionadas así:
Ejemplo: excluir categorías 10, 11, 12
$args = array(
'category__not_in' => array('10','11','12')
);
Digamos que tengo un post marcado en categoría 12 y 13
. Con el código anterior no obtendría ese post porque excluye posts de la categoría 12
. Pero idealmente debería aparecer ya que la categoría 13
no fue deseleccionada.
Como solución podría usar 'category__in'
con todos los IDs de categorías seleccionadas. Pero me preocupa que la lista sea muy larga (aunque se genere programáticamente) y no estoy seguro del impacto en el rendimiento de WP_Query
al tener más de 300 categorías.
¿Alguien tiene una mejor idea para resolver este problema?

Como probablemente ya sabes, las categorías son taxonomías. Cuando utilizas argumentos como category__in
, esto agregará una consulta de taxonomía a tu WP_Query()
. Entonces, tu situación sería algo así:
$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 );
No consideraría que haya problemas de rendimiento aquí. Esta es muy probablemente tu única solución, si no deseas consultar directamente las publicaciones desde la base de datos utilizando una consulta SQL (esto podría mejorar un poco el rendimiento).

¿Pero no crees que siempre tendremos el mismo problema? El OP quería usar algo para evitar listar todas las categorías, pero en tu caso estarás obligado a listar las categorías dentro de la parte IN, ¿verdad?

category__in
o category__and
es lo que necesitas aquí como dice Jack. Si buscas acelerar tus consultas, quizás considera usar transients.

Además, la respuesta de graziondev a continuación también es correcta, no hay necesidad de usar category__in
cuando haces tu consulta inicial ya que estás incluyendo todas las categorías por defecto. Carga esa consulta en tu transitorio y luego filtra solicitudes posteriores desde esos datos si deseas velocidad.

Supongamos que tenemos 4 publicaciones y 4 categorías.
+----+--------+
| ID | Post |
+----+--------+
| 1 | Test 1 |
| 2 | Test 2 |
| 3 | Test 3 |
| 4 | Test 4 |
+----+--------+
+----+------------+
| ID | Category |
+----+------------+
| 1 | Category 1 |
| 2 | Category 2 |
| 3 | Category 3 |
| 4 | Category 4 |
+----+------------+
+--------+------------------------+
| Post | Category |
+--------+------------------------+
| Test 1 | Category 1, Category 2 |
| Test 2 | Category 2 |
| Test 3 | Category 3 |
| Test 4 | Category 4 |
+--------+------------------------+
Si entendí correctamente tu pregunta, deseas obtener la publicación Test 1
usando el parámetro category__not_in
. Los argumentos para tu consulta se verían así:
$args = array(
'category__not_in' => array(2, 3, 4)
);
El problema con category__not_in
es que produce una consulta 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
excluirá todas las publicaciones, incluyendo Test 1
. Si solo esta consulta SQL usara JOIN
en lugar de NOT IN SELECT
, esto funcionaría.
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
La consulta SQL anterior solo devolverá la publicación Test 1
. Podemos hacer un pequeño truco para producir dicha consulta usando la clase WP_Query. En lugar de usar el parámetro category__not_in
, reemplázalo con el parámetro category__in
y agrega un filtro post_where
que modificará directamente el SQL para nuestro propósito.
function wp_286618_get_posts() {
$query = new WP_Query( array(
'post_type' => 'post',
'category__in' => array( 2, 3, 4 ) // Usa `category__in` para forzar la consulta SQL JOIN.
) );
return $query->get_posts();
}
function wp_286618_replace_in_operator($where, $object) {
$search = 'term_taxonomy_id IN'; // Busca el operador IN creado por el parámetro `category__in`.
$replace = 'term_taxonomy_id NOT IN'; // Reemplaza el operador IN por NOT IN
$where = str_replace($search, $replace, $where);
return $where;
}
add_filter( 'posts_where', 'wp_286618_replace_in_operator', 10, 2 ); // Agrega el filtro para reemplazar el operador IN
$posts = wp_286618_get_posts(); // Devolverá solo la publicación Test 1
remove_filter( 'posts_where', 'wp_286618_replace_in_operator', 10, 2 ); // Elimina el filtro para no afectar otras consultas
La ventaja de esta solución sobre otras es que no necesito conocer los ID de otras categorías, y mantendrá tu bucle de publicaciones limpio.

¿Por qué tienes todas las categorías pre-seleccionadas? ¿No sería más fácil mostrar todos los resultados y al mismo tiempo tener las categorías sin seleccionar? Luego, cuando el usuario seleccione algunas (¿quién va a seleccionar 300 categorías?), puedes ejecutar una consulta con category__in
.

no es una opción para mí. normalmente la gente tiende a seleccionar casi todas las categorías, pero algunos no quieren ver publicaciones innecesarias, así que en ese caso podrían deseleccionar las categorías. Pero en mi opinión, más del 90% de las categorías estarían seleccionadas. Esa es la razón por la que damos la opción de deseleccionar en lugar de seleccionar.

No estoy seguro, pero creo que no puedes hacer esto usando el comportamiento/opciones predeterminadas de WP_Query
. Así que tal vez una solución alternativa sería implementar una función que haga esta prueba por ti después de seleccionar todas las publicaciones.
Por supuesto, la desventaja de este método es que primero tienes que seleccionar todas las publicaciones y luego filtrarlas, pero puede ser una solución para tu problema. Entonces podrías hacer algo como esto:
<?php
function test_categorie($cats,$ex_cats) {
$test = false; //consideramos que esta publicación está excluida hasta que encontremos una categoría que no pertenezca al array
foreach ($cats as $cat){
if(!in_array(intval($cat->term_id),$ex_cats)) {
$test = true;
//Podemos salir del bucle ya que esta publicación tiene al menos una categoría no excluida, así que podemos considerarla
break;
}
}
return $test;
}
//Define las categorías excluidas aquí
$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();
//hacemos nuestra prueba, si es false no consideramos la publicación y continuamos con la siguiente
if(!test_categorie(get_the_category(),$exclude_cat))
continue;
/*
Añade tu código aquí
*/
endwhile;
wp_reset_postdata();
endif;
?>

Esta respuesta funcionará, creo, aunque puedes encontrarte con algunos problemas si el uso o la salida depende de que la consulta sea sólida en relación con la exclusión, por ejemplo, para una paginación precisa: Podrías obtener algunas páginas que deberían mostrar, digamos, 5 elementos, pero en su lugar muestran 2, por ejemplo. Por supuesto, también hay soluciones para eso, aunque siempre habrá compensaciones de un tipo u otro.

Prueba este método. Reconozco que puede no ser la solución más limpia, ¡pero a mí me funciona perfectamente!
$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)) {
// Implementa aquí tu propio código
}
}
}
