WP_Query: как сравнить мета-поля между собой в meta_query запросе
WP_Query из коробки не позволяет делать сравнения между значениями метаполей с которыми мы работаем.
Для метаполей в compare
можно указать BETWEEN, >, >=, <, <=. Но в значении (параметре value) мы должны указать какие-то конкретные данные. А если нам нужно в значении указать значение другого метаполя для сравнения, то WP тут бессилен и придется изменять сам запрос, а для неопытных это боль и возможно просто неразрешимая задача.
Пример, когда такое может пригодится (вопрос из комментария).
В карточке продукта указано два мета поля. (Этаж "room_floor" и Максимальный этаж в доме "room_floor_max".)
Для фильтрации объектов выборка идет по этажности ("Не первый этаж" - отрабатывает конкретно, так как передаю в value значение 1 этажа)
Вопрос, как мне скорректировать обработку последнего этажа? По сути нужно сравнить два мета поля ("room_floor" и "room_floor_max"), если они одинаковы, то я их не вывожу. Например есть объект на 5 этаже из 5 максимальных (5/5), этот объект подходит для фильтра
Решить эту проблему можно так (нечто подобное вы сможете найти в "интернетах"):
$args = [ 'post_type' => 'post', 'posts_per_page' => 50, 'meta_query' => [ 'relation' => 'AND', // wp_postmeta.meta_value [ 'key' => 'room_floor', 'value' => 1, 'compare' => '!=', 'type' => 'NUMERIC', ], // mt1.meta_value [ 'key' => 'room_floor_max', 'value' => 'wp_postmeta.meta_value', 'compare' => '!=', 'type' => 'NUMERIC', ], ], ]; $closure = function( $clauses ) { //$clauses['where'] = preg_replace( "/'mt(\d+)\.meta_value'/", 'mt$1.meta_value', $clauses['where'] ); $clauses['where'] = str_replace( "'wp_postmeta.meta_value'", 'wp_postmeta.meta_value', $clauses['where'] ); return $clauses; }; add_filter( 'posts_clauses', $closure ); $my_posts = get_posts( [ 'suppress_filters' => false ] + $args ); remove_filter( 'posts_clauses', $closure ); foreach( $my_posts as $pst ){ echo "$pst->post_title\n"; }
Т.е. здесь в значении мы в value указываем 'wp_postmeta.meta_value'
- это имя колонки которое в итоге будет использоваться в SQL запросе. Но это имя будет использовано в запросе как строка, а нам нужно использовать его как есть, для этого на хуке posts_clauses_request, мы изменяем запрос, и превращаем строку 'wp_postmeta.meta_value'
в реальную часть запроса wp_postmeta.meta_value
.
Универсальный вариант
Рекомендую использовать именно этот вариант. Потому что он:
- Более понятен при использовании.
- Удобен - потому что не надо выяснять какой префикс у метаполя в запросе.
- Стабилен - при изменении/расширении параметров meta_query не сломается то, что уже работало.
Прием который показан выше можно сделать более универсальным - дать имя указанному в meta_query
массиву и использовать это имя в другом массиве meta_query
, для параметра value
. Затем заменять это имя на реальное название колонки которое получилось при сборке запроса. Плюс такого подхода в том, что не нужно будет знать какой конкретно у колонки meta_value
префикс (а он может быть: mt1, mt2, wp_postmeta или какой-то другой).
Другой хрупкий момент - при изменении кода (параметров запроса). Например при добавлении еще одного массива в meta_query
или изменении relation на OR, префикс mt1
может измениться в результирующем SQL запросе и ваш код перестанет работать.
Код ниже лишен всех этих недостатков! А так же параметры запроса становятся более понятными.
К слову, аналогичный подход уже используется в параметре orderby. Он позволяет сортировать запрос по конкретному метаполю из параметра meta_query
.
/** * Allows to use `meta_query` parameter item key as `value` * parameter in another `meta_query` item for compare meta * values between each other. * * Example: * * 'meta_query' => [ * 'relation' => 'AND', * 'room_floor.value' => [ * 'key' => 'room_floor', * 'value' => 1, * 'compare' => '!=', * 'type' => 'NUMERIC', * ], * [ * 'key' => 'room_floor_max', * 'compare' => '!=', * 'value' => 'room_floor.value', * 'type' => 'NUMERIC', * ], * ], * * // in this example we specified `room_floor.value` key * // and then use it as `value` for another item. * * @requires PHP 7.0 */ final class WP_Query_Allow_Postmeta_Compare { public static function init(){ add_filter( 'posts_clauses', [ __CLASS__, '_meta_value_replacer' ], 20, 2 ); //add_filter( 'posts_request', [ __CLASS__, 'debug_die_request' ], 999 ); } public static function _meta_value_replacer( $clauses, $wp_query ){ /** @var WP_Meta_Query $mq */ $mq = $wp_query->meta_query; if( ! $mq ){ return $clauses; } $mq_clauses = $mq->get_clauses() ?: []; $replace = []; foreach( $mq_clauses as $key => $clause ){ if( $clause['key'] === $key ){ trigger_error( "`Meta clause key` can not be the same as value parameter. The key: $key" ); continue; } $value = $clause['value'] ?? ''; $the_clause = $mq_clauses[ $value ] ?? []; if( ! $the_clause ){ continue; } $from = "'$value'"; if( 'CHAR' === $the_clause['cast'] ){ $to = sprintf( '%s.meta_value', $the_clause['alias'] ); } else { $to = sprintf( 'CAST( %s.meta_value AS %s )', $the_clause['alias'], $the_clause['cast'] ); } $replace[ $from ] = $to; } foreach( $replace as $from => $to ){ $clauses['where'] = str_replace( $from, $to, $clauses['where'] ); } return $clauses; } public static function debug_die_request( $sql ){ die( $sql ); } }
Как пользоваться?
Подключаем код класса куда-нибудь (лучше в файл и файл подключить в functions.php). Затем где-нибудь в functions.php запускаем класс (на другие запросы он влиять не будет, потому что они не подойдут под условия):
WP_Query_Allow_Postmeta_Compare::init();
Теперь в запросах get_posts() или WP_Query, в параметре meta_query можно указывать ключ для отдельного элемента и использовать этот ключ в значении другого элемента, чтобы сравнивать значения метаполей между собой по указанному в compare
условию. Также учитывается параметр type
, если он например NUMERIC, то значения будут сравниваться как числа.
Пример запроса:
$args = [ 'post_type' => 'post', 'posts_per_page' => 50, 'meta_query' => [ 'relation' => 'AND', 'room_floor.value' => [ 'key' => 'room_floor', 'value' => 1, 'compare' => '!=', 'type' => 'NUMERIC', ], [ 'key' => 'room_floor_max', 'compare' => '!=', 'value' => 'room_floor.value', 'type' => 'NUMERIC', ], ], ]; $my_posts = get_posts( [ 'suppress_filters' => false ] + $args ); foreach( $my_posts as $pst ){ echo "$pst->post_title\n"; }
Тут мы указали произвольный ключ room_floor.value
для элемента массива, а затем использовали его в значении другого элемента: 'value' => 'room_floor.value'
.
Критически важные моменты:
-
Название ключа (в примере
room_floor.value
) не должно совпадать с названием самого метаполя (в примереroom_floor
)! - Параметр запроса
suppress_filters
должен бытьfalse
. Потому что этот хак работает на хуках и еслиsuppress_filters=true
нужные хуки не будут запускаться. По умолчанию в функции get_posts() онtrue
.
Для WP_Query suppress_filters=false
указывать не обязательно.
$query = new WP_Query( $args ); if ( $query->have_posts() ) { while ( $query->have_posts() ) { $query->the_post(); ?> <li><?php the_title() ?></li> <?php } } else { // Постов не найдено } wp_reset_postdata(); // Сбрасываем $post.