Files
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

213 lines
6.6 KiB
PHP

<?php
/**
* Database abstraction layer for WP Data Optimizer.
*
* @package WP_Data_Optimizer
*/
declare(strict_types=1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* TMDO_DB — Database abstraction layer for MySQL / SQLite dual support.
*
* Provides unified transaction interface, SQL dialect helpers, and
* table name resolution. Absorbs and extends the FCB_DB pattern.
*/
class TMDO_DB {
/**
* Check if the current database engine is SQLite.
*/
public static function is_sqlite(): bool {
return TMDO_IS_SQLITE;
}
/**
* Check if the current database engine is MySQL.
*/
public static function is_mysql(): bool {
return TMDO_IS_MYSQL;
}
/**
* Begin a database transaction.
*
* SQLite: BEGIN IMMEDIATE (write-lock, prevents concurrent writes).
* MySQL: START TRANSACTION (row-level locking via InnoDB).
*/
public static function begin(): void {
global $wpdb;
$wpdb->query( self::is_sqlite() ? 'BEGIN IMMEDIATE' : 'START TRANSACTION' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static SQL literals; no user input.
}
/**
* Commit the current transaction.
*/
public static function commit(): void {
global $wpdb;
$wpdb->query( 'COMMIT' );
}
/**
* Roll back the current transaction.
*/
public static function rollback(): void {
global $wpdb;
$wpdb->query( 'ROLLBACK' );
}
/**
* Get the current WordPress local time as a MySQL datetime string.
*/
public static function now(): string {
return current_time( 'mysql' );
}
/**
* Get the full table name with wpdb prefix.
*
* @param string $name Table name without prefix (e.g. 'wpdo_warm').
* @return string Fully-prefixed, sanitized table name.
*/
public static function table( string $name ): string {
global $wpdb;
return $wpdb->prefix . sanitize_key( $name );
}
/**
* Execute an INSERT IGNORE statement (MySQL) or INSERT OR IGNORE (SQLite).
*
* @param string $table Full table name.
* @param array $data Column => value pairs.
* @param array $format Optional wpdb format array ('%s', '%d', etc.).
* @return int|false Number of rows affected, or false on error.
*/
public static function insert_ignore( string $table, array $data, array $format = array() ): int|false {
global $wpdb;
$columns = array_keys( $data );
$values = array_values( $data );
$col_list = implode( ', ', array_map( fn( $c ) => '`' . sanitize_key( $c ) . '`', $columns ) );
$placeholder = implode( ', ', $format ?: array_fill( 0, count( $values ), '%s' ) );
$keyword = self::is_sqlite() ? 'INSERT OR IGNORE' : 'INSERT IGNORE';
$sql = "{$keyword} INTO `{$table}` ({$col_list}) VALUES ({$placeholder})";
return $wpdb->query( $wpdb->prepare( $sql, ...$values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Build a JSON_EXTRACT expression compatible with both MySQL and SQLite.
*
* Both MySQL 5.7+ and SQLite 3.38+ support json_extract().
*
* @param string $column Column name containing JSON data.
* @param string $path JSON path (e.g. '$.social_links').
* @return string SQL expression.
*/
public static function json_extract( string $column, string $path ): string {
$column = sanitize_key( $column );
// JSON path must start with '$' and contain only word chars, dots, brackets, and digits.
if ( ! preg_match( '/^\$[\w.\[\]0-9]*$/', $path ) ) {
$path = '$';
}
return "json_extract(`{$column}`, '{$path}')";
}
/**
* Build an UPSERT statement.
*
* MySQL: INSERT ... ON DUPLICATE KEY UPDATE ...
* SQLite: INSERT ... ON CONFLICT(...) DO UPDATE SET ...
*
* @param string $table Full table name.
* @param array $data Column => value pairs to insert.
* @param array $update_columns Columns to update on conflict.
* @param string|string[] $conflict_key Column name (string) or columns (array)
* for ON CONFLICT (SQLite). On MySQL it
* is informational only — ON DUPLICATE
* KEY UPDATE matches any unique key.
* @param array $format Optional wpdb format array.
* @return int|false
*/
public static function upsert( string $table, array $data, array $update_columns, string|array $conflict_key = 'post_id', array $format = array() ): int|false {
global $wpdb;
$columns = array_keys( $data );
$values = array_values( $data );
// Normalise composite vs single conflict key.
$conflict_keys = is_array( $conflict_key ) ? $conflict_key : array( $conflict_key );
$conflict_keys = array_map( 'sanitize_key', $conflict_keys );
$conflict_list = implode(
', ',
array_map( static fn( $k ) => '`' . $k . '`', $conflict_keys )
);
$col_list = implode( ', ', array_map( fn( $c ) => '`' . sanitize_key( $c ) . '`', $columns ) );
// Build per-value placeholders, using NULL literal for null values.
$prepare_values = array();
$placeholders = array();
$fmt = $format ?: array_fill( 0, count( $values ), '%s' );
foreach ( $values as $i => $v ) {
if ( null === $v ) {
$placeholders[] = 'NULL';
} else {
$placeholders[] = $fmt[ $i ] ?? '%s';
$prepare_values[] = $v;
}
}
$placeholder = implode( ', ', $placeholders );
// ON DUPLICATE KEY UPDATE: null columns use NULL literal, others use VALUES().
$null_cols = array();
foreach ( $update_columns as $c ) {
$col_index = array_search( $c, $columns, true );
if ( false !== $col_index && null === $values[ $col_index ] ) {
$null_cols[] = $c;
}
}
if ( self::is_mysql() ) {
$updates = implode(
', ',
array_map(
function ( $c ) use ( $null_cols ) {
$safe = sanitize_key( $c );
return in_array( $c, $null_cols, true )
? "`{$safe}` = NULL"
: "`{$safe}` = VALUES(`{$safe}`)";
},
$update_columns
)
);
$sql = "INSERT INTO `{$table}` ({$col_list}) VALUES ({$placeholder}) ON DUPLICATE KEY UPDATE {$updates}";
} else {
$updates = implode(
', ',
array_map(
function ( $c ) use ( $null_cols ) {
$safe = sanitize_key( $c );
return in_array( $c, $null_cols, true )
? "`{$safe}` = NULL"
: "`{$safe}` = excluded.`{$safe}`";
},
$update_columns
)
);
$sql = "INSERT INTO `{$table}` ({$col_list}) VALUES ({$placeholder}) ON CONFLICT({$conflict_list}) DO UPDATE SET {$updates}";
}
if ( empty( $prepare_values ) ) {
return $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
return $wpdb->query( $wpdb->prepare( $sql, ...$prepare_values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
}