WordPress как на ладони
Очень Удобный и Быстрый Хостинг для сайтов на WordPress. Пользуюсь сам и вам рекомендую!

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.

/**
 * Allowes 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.
6 комментариев
    Войти