Files
2meet-data-optimizer/includes/zones/class-tmdo-zone-hot.php
T
wpdev 76c01e44df refactor: 全部 128 個生產檔加入 declare(strict_types=1)(PR-H)
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入
並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 06:13:33 +08:00

215 lines
6.3 KiB
PHP

<?php
/**
* Zone A (Hot) handler for flat-column custom tables.
*
* @package WP_Data_Optimizer
*/
declare(strict_types=1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Zone A (Hot) handler — flat-column custom tables for search/filter fields.
*
* Each post type gets its own table: wpdo_hot_{post_type}
* Columns are defined by the Schema Registry and created via TMDO_Installer.
*
* Key advantage over HPCT's KV table (hpct_listing_meta):
* KV table: N meta_query conditions = N LEFT JOINs
* Flat table: N conditions = 1 LEFT JOIN + N WHERE clauses
*/
class TMDO_Zone_Hot {
/**
* Get the table name for a post type.
*
* @param string $post_type Post type slug.
* @return string Full table name.
*/
public static function table( string $post_type ): string {
return TMDO_DB::table( 'wpdo_hot_' . sanitize_key( $post_type ) );
}
/**
* Ensure the hot table exists for a post type.
* Creates it dynamically from Schema Registry if missing.
*
* @param string $post_type Post type slug.
* @return void
*/
public static function ensure_table( string $post_type ): void {
$columns = TMDO_Schema_Registry::instance()->get_hot_columns( $post_type );
if ( empty( $columns ) ) {
return;
}
global $wpdb;
$table = self::table( $post_type );
// Quick existence check.
if ( TMDO_IS_SQLITE ) {
$exists = $wpdb->get_var(
$wpdb->prepare( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=%s", $table )
);
} else {
$exists = $wpdb->get_var(
$wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $table )
);
}
if ( ! $exists ) {
TMDO_Installer::create_hot_table( $post_type, $columns );
return;
}
// v2.1.2 critical fix: ensure existing tables have all columns declared
// by Schema_Registry. Partner plugins registering new hot fields after
// the initial migration would otherwise silently fall back to postmeta.
//
// v2.1.3 optimization: schema fingerprint stored in option. When the hash
// of declared columns matches the stored hash, skip SHOW COLUMNS entirely.
// Only diff + ALTER fires when fingerprint actually changed (drift detected).
// This avoids per-request SHOW COLUMNS overhead on stable production sites.
static $checked = array();
if ( isset( $checked[ $post_type ] ) ) {
return;
}
$fingerprint = self::compute_fingerprint( $columns );
$opt_key = 'wpdo_hot_fp_' . $post_type;
$stored_fp = (string) get_option( $opt_key, '' );
if ( $stored_fp === $fingerprint ) {
// Schema unchanged since last successful ensure → no need to SHOW COLUMNS.
$checked[ $post_type ] = true;
return;
}
// Fingerprint mismatch (or first run) → run diff + ALTER, then store new hash.
TMDO_Installer::ensure_hot_columns( $post_type, $columns );
update_option( $opt_key, $fingerprint, false );
$checked[ $post_type ] = true;
}
/**
* Stable hash of declared column definitions. Used to short-circuit SHOW COLUMNS
* when Schema_Registry hasn't changed since the last successful ensure.
*
* @param array $columns column_name => sql_type map.
* @return string Short SHA-1 prefix (10 chars — collision-safe at our scale).
*
* @since 2.1.3
*/
private static function compute_fingerprint( array $columns ): string {
ksort( $columns );
return substr( sha1( wp_json_encode( $columns ) ?: '' ), 0, 10 );
}
/**
* Read a single field value from the hot table.
*
* @param int $post_id Post ID.
* @param string $post_type Post type.
* @param string $column Column name in hot table.
* @return mixed|null Value or null if not found.
*/
public static function get( int $post_id, string $post_type, string $column ): mixed {
global $wpdb;
$table = self::table( $post_type );
$column = sanitize_key( $column );
return $wpdb->get_var(
$wpdb->prepare(
"SELECT `{$column}` FROM `{$table}` WHERE post_id = %d LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- column from sanitize_key(); table from TMDO_DB::table().
$post_id
)
);
}
/**
* Read all hot fields for a post as an associative array.
*
* @param int $post_id Post ID.
* @param string $post_type Post type.
* @return array|null Column => value pairs, or null.
*/
public static function get_row( int $post_id, string $post_type ): ?array {
global $wpdb;
$table = self::table( $post_type );
$row = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM `{$table}` WHERE post_id = %d LIMIT 1", $post_id ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from TMDO_Zone_Hot::table() via TMDO_DB::table().
ARRAY_A
);
return $row ?: null;
}
/**
* Upsert a single field into the hot table.
*
* @param int $post_id Post ID.
* @param string $post_type Post type.
* @param string $column Column name.
* @param mixed $value Value to set.
*/
public static function set( int $post_id, string $post_type, string $column, mixed $value ): void {
$table = self::table( $post_type );
$column = sanitize_key( $column );
$now = TMDO_DB::now();
TMDO_DB::upsert(
$table,
array(
'post_id' => $post_id,
$column => $value,
'updated_at' => $now,
),
array( $column, 'updated_at' ),
'post_id'
);
}
/**
* Upsert multiple fields at once for a post.
*
* @param int $post_id Post ID.
* @param string $post_type Post type.
* @param array $data Column => value pairs.
*/
public static function set_many( int $post_id, string $post_type, array $data ): void {
$table = self::table( $post_type );
$now = TMDO_DB::now();
$data['post_id'] = $post_id;
$data['updated_at'] = $now;
$update_cols = array_diff( array_keys( $data ), array( 'post_id' ) );
TMDO_DB::upsert( $table, $data, $update_cols, 'post_id' );
}
/**
* Delete a post's row from the hot table.
*
* @param int $post_id Post ID.
* @param string $post_type Post type slug.
* @return void
*/
public static function delete( int $post_id, string $post_type ): void {
global $wpdb;
$wpdb->delete( self::table( $post_type ), array( 'post_id' => $post_id ), array( '%d' ) );
}
/**
* Get all post types that have hot zone tables registered.
*
* @return string[]
*/
public static function get_post_types(): array {
return TMDO_Schema_Registry::instance()->get_hot_post_types();
}
}