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.



