d36bb954d1
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
323 lines
8.9 KiB
PHP
323 lines
8.9 KiB
PHP
<?php
|
||
/**
|
||
* TMDO_Query_Compiler - 跨實體查詢編譯器
|
||
*
|
||
* 將 wpdo_meta_query 編譯為高效 SQL:
|
||
* - 單次 JOIN UAE 表(而非原生 meta_query 多次 JOIN wp_*meta)
|
||
* - 型別正確的比較(避免 CAST 開銷)
|
||
* - 命中索引
|
||
*
|
||
* @package WP_Data_Optimizer
|
||
*/
|
||
|
||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||
|
||
|
||
defined( 'ABSPATH' ) || exit;
|
||
|
||
final class TMDO_Query_Compiler {
|
||
|
||
/** @var array 合法的 SQL 比較運算子 */
|
||
private const VALID_OPERATORS = array(
|
||
'=',
|
||
'!=',
|
||
'<>',
|
||
'>',
|
||
'>=',
|
||
'<',
|
||
'<=',
|
||
'LIKE',
|
||
'NOT LIKE',
|
||
'IN',
|
||
'NOT IN',
|
||
'BETWEEN',
|
||
'NOT BETWEEN',
|
||
'EXISTS',
|
||
'NOT EXISTS',
|
||
);
|
||
|
||
/**
|
||
* 注入 wpdo_meta_query 至 WP_Query(post 實體)
|
||
*
|
||
* @param WP_Query $query
|
||
* @param array $wpdo_meta_query 結構同 meta_query
|
||
*/
|
||
public static function inject_into_wp_query( WP_Query $query, array $wpdo_meta_query ): void {
|
||
|
||
$post_type = $query->get( 'post_type' );
|
||
if ( is_array( $post_type ) ) {
|
||
$post_type = $post_type[0] ?? 'post';
|
||
}
|
||
if ( empty( $post_type ) || $post_type === 'any' ) {
|
||
$post_type = 'post';
|
||
}
|
||
|
||
// 編譯 JOIN 與 WHERE
|
||
$compiled = self::compile( 'post', $wpdo_meta_query );
|
||
|
||
if ( empty( $compiled['joins'] ) && empty( $compiled['where'] ) ) {
|
||
return;
|
||
}
|
||
|
||
// 注入 posts_clauses filter
|
||
add_filter(
|
||
'posts_clauses',
|
||
function ( $clauses ) use ( $compiled ) {
|
||
global $wpdb;
|
||
|
||
if ( ! empty( $compiled['joins'] ) ) {
|
||
$clauses['join'] .= ' ' . implode( ' ', $compiled['joins'] );
|
||
}
|
||
|
||
if ( ! empty( $compiled['where'] ) ) {
|
||
$clauses['where'] .= ' AND (' . implode( ' AND ', $compiled['where'] ) . ')';
|
||
}
|
||
|
||
return $clauses;
|
||
},
|
||
10,
|
||
1
|
||
);
|
||
|
||
// wpdo_orderby 支援
|
||
$wpdo_orderby = $query->get( 'wpdo_orderby' );
|
||
if ( $wpdo_orderby ) {
|
||
$order_dir = strtoupper( $query->get( 'order' ) ?: 'DESC' );
|
||
if ( ! in_array( $order_dir, array( 'ASC', 'DESC' ), true ) ) {
|
||
$order_dir = 'DESC';
|
||
}
|
||
|
||
$wpdo_col = TMDO_Schema_Manager::sanitize_column_name( $wpdo_orderby );
|
||
|
||
// 找出該欄位所在的群組
|
||
$field_def = TMDO_Entity_Registry::get_field( 'post', $wpdo_orderby );
|
||
if ( $field_def ) {
|
||
$alias = 'wpdo_' . $field_def['group'];
|
||
add_filter(
|
||
'posts_orderby',
|
||
function ( $orderby ) use ( $alias, $wpdo_col, $order_dir ) {
|
||
return "`{$alias}`.`{$wpdo_col}` {$order_dir}";
|
||
},
|
||
10,
|
||
1
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 注入至 WP_User_Query
|
||
*/
|
||
public static function inject_into_user_query( WP_User_Query $query, array $wpdo_meta_query ): void {
|
||
|
||
$compiled = self::compile( 'user', $wpdo_meta_query );
|
||
|
||
if ( empty( $compiled['joins'] ) && empty( $compiled['where'] ) ) {
|
||
return;
|
||
}
|
||
|
||
global $wpdb;
|
||
|
||
// 透過 reflection 存取 WP_User_Query 的 query_vars 來注入
|
||
$query_orderby = &$query->query_orderby;
|
||
$query_where = &$query->query_where;
|
||
$query_from = &$query->query_from;
|
||
|
||
if ( ! empty( $compiled['joins'] ) ) {
|
||
$query_from .= ' ' . implode( ' ', $compiled['joins'] );
|
||
}
|
||
|
||
if ( ! empty( $compiled['where'] ) ) {
|
||
$query_where .= ' AND (' . implode( ' AND ', $compiled['where'] ) . ')';
|
||
}
|
||
|
||
// wpdo_orderby
|
||
$wpdo_orderby = $query->get( 'wpdo_orderby' );
|
||
if ( $wpdo_orderby ) {
|
||
$field_def = TMDO_Entity_Registry::get_field( 'user', $wpdo_orderby );
|
||
if ( $field_def ) {
|
||
$order_dir = strtoupper( $query->get( 'order' ) ?: 'DESC' );
|
||
if ( ! in_array( $order_dir, array( 'ASC', 'DESC' ), true ) ) {
|
||
$order_dir = 'DESC';
|
||
}
|
||
$alias = 'wpdo_' . $field_def['group'];
|
||
$col = TMDO_Schema_Manager::sanitize_column_name( $wpdo_orderby );
|
||
$query_orderby = "ORDER BY `{$alias}`.`{$col}` {$order_dir}";
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 注入至 get_terms() 的 clauses
|
||
*/
|
||
public static function inject_into_terms_clauses( array $clauses, array $wpdo_meta_query ): array {
|
||
|
||
$compiled = self::compile( 'term', $wpdo_meta_query );
|
||
|
||
if ( empty( $compiled['joins'] ) && empty( $compiled['where'] ) ) {
|
||
return $clauses;
|
||
}
|
||
|
||
if ( ! empty( $compiled['joins'] ) ) {
|
||
$clauses['join'] .= ' ' . implode( ' ', $compiled['joins'] );
|
||
}
|
||
|
||
if ( ! empty( $compiled['where'] ) ) {
|
||
$clauses['where'] .= ' AND (' . implode( ' AND ', $compiled['where'] ) . ')';
|
||
}
|
||
|
||
return $clauses;
|
||
}
|
||
|
||
/**
|
||
* 核心編譯邏輯
|
||
*
|
||
* @param string $entity_type
|
||
* @param array $meta_query 結構範例:
|
||
* [
|
||
* 'relation' => 'AND',
|
||
* [ 'key' => 'price', 'value' => [100, 500], 'compare' => 'BETWEEN' ],
|
||
* [ 'key' => 'stock', 'value' => 'instock' ],
|
||
* ]
|
||
* @return array{joins: array<string>, where: array<string>}
|
||
*/
|
||
public static function compile( string $entity_type, array $meta_query ): array {
|
||
|
||
$adapter = TMDO_Entity_Registry::get_adapter( $entity_type );
|
||
if ( ! $adapter ) {
|
||
return array(
|
||
'joins' => array(),
|
||
'where' => array(),
|
||
);
|
||
}
|
||
|
||
$relation = strtoupper( $meta_query['relation'] ?? 'AND' );
|
||
if ( ! in_array( $relation, array( 'AND', 'OR' ), true ) ) {
|
||
$relation = 'AND';
|
||
}
|
||
unset( $meta_query['relation'] );
|
||
|
||
$needed_groups = array(); // group → true
|
||
$where_clauses = array();
|
||
|
||
foreach ( $meta_query as $clause ) {
|
||
if ( ! is_array( $clause ) || empty( $clause['key'] ) ) {
|
||
continue;
|
||
}
|
||
|
||
$field_def = TMDO_Entity_Registry::get_field( $entity_type, $clause['key'] );
|
||
if ( ! $field_def ) {
|
||
// 非 UAE 管理的欄位,跳過(讓原生 meta_query 接手)
|
||
continue;
|
||
}
|
||
|
||
$group = $field_def['group'];
|
||
$needed_groups[ $group ] = true;
|
||
|
||
$compare = strtoupper( $clause['compare'] ?? '=' );
|
||
if ( ! in_array( $compare, self::VALID_OPERATORS, true ) ) {
|
||
$compare = '=';
|
||
}
|
||
|
||
$alias = 'wpdo_' . $group;
|
||
$col = TMDO_Schema_Manager::sanitize_column_name( $clause['key'] );
|
||
$value = $clause['value'] ?? null;
|
||
|
||
$where_clauses[] = self::build_comparison( $alias, $col, $compare, $value, $field_def );
|
||
}
|
||
|
||
// 建立 JOIN
|
||
$joins = array();
|
||
$prim_table = $adapter->get_primary_table();
|
||
$prim_id = $adapter->get_primary_id_column();
|
||
$id_col = $adapter->get_entity_id_column();
|
||
|
||
foreach ( array_keys( $needed_groups ) as $group ) {
|
||
$wpdo_table = TMDO_Schema_Manager::get_table_name( $entity_type, $group );
|
||
|
||
if ( ! TMDO_Schema_Manager::table_exists( $wpdo_table ) ) {
|
||
continue;
|
||
}
|
||
|
||
$alias = 'wpdo_' . $group;
|
||
$joins[] = "LEFT JOIN `{$wpdo_table}` `{$alias}` ON `{$prim_table}`.`{$prim_id}` = `{$alias}`.`{$id_col}`";
|
||
}
|
||
|
||
// 組合 where,依 relation 連接
|
||
$where_sql = array();
|
||
if ( ! empty( $where_clauses ) ) {
|
||
$where_sql[] = implode( " {$relation} ", $where_clauses );
|
||
}
|
||
|
||
return array(
|
||
'joins' => $joins,
|
||
'where' => $where_sql,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 產生單一比較子句
|
||
*/
|
||
private static function build_comparison(
|
||
string $alias,
|
||
string $col,
|
||
string $compare,
|
||
$value,
|
||
array $field_def
|
||
): string {
|
||
global $wpdb;
|
||
|
||
$col_ref = "`{$alias}`.`{$col}`";
|
||
|
||
switch ( $compare ) {
|
||
case 'EXISTS':
|
||
return "{$col_ref} IS NOT NULL";
|
||
|
||
case 'NOT EXISTS':
|
||
return "{$col_ref} IS NULL";
|
||
|
||
case 'BETWEEN':
|
||
case 'NOT BETWEEN':
|
||
if ( ! is_array( $value ) || count( $value ) !== 2 ) {
|
||
return '1=1';
|
||
}
|
||
$v1 = self::escape_value( $value[0], $field_def );
|
||
$v2 = self::escape_value( $value[1], $field_def );
|
||
return "{$col_ref} {$compare} {$v1} AND {$v2}";
|
||
|
||
case 'IN':
|
||
case 'NOT IN':
|
||
if ( ! is_array( $value ) ) {
|
||
$value = array( $value );
|
||
}
|
||
if ( empty( $value ) ) {
|
||
return $compare === 'IN' ? '1=0' : '1=1';
|
||
}
|
||
$escaped = array_map( fn( $v ) => self::escape_value( $v, $field_def ), $value );
|
||
return "{$col_ref} {$compare} (" . implode( ',', $escaped ) . ')';
|
||
|
||
case 'LIKE':
|
||
case 'NOT LIKE':
|
||
$like_val = '%' . $wpdb->esc_like( (string) $value ) . '%';
|
||
return "{$col_ref} {$compare} '" . esc_sql( $like_val ) . "'";
|
||
|
||
default:
|
||
$escaped = self::escape_value( $value, $field_def );
|
||
return "{$col_ref} {$compare} {$escaped}";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 型別安全地轉義值
|
||
*/
|
||
private static function escape_value( $value, array $field_def ): string {
|
||
$type = $field_def['type'] ?? 'text';
|
||
|
||
return match ( $type ) {
|
||
'integer', 'boolean' => (string) (int) $value,
|
||
'decimal' => (string) (float) $value,
|
||
default => "'" . esc_sql( (string) $value ) . "'",
|
||
};
|
||
}
|
||
}
|