', '>', '>=', '<', '<=', '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, where: array} */ 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 ) . "'", }; } }