Files
2meet-data-optimizer/modules/options/class-tmdo-options-manager.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

288 lines
8.1 KiB
PHP

<?php
/**
* TMDO_Options_Manager - wp_options 反 EAV 模組
*
* 功能:
* - autoload 最佳化(掃描並 defer 過大的 autoload 項目)
* - 選項重導向(將高頻選項搬至專屬設定表)
* - 快取攔截
*
* @package WP_Data_Optimizer
*/
declare(strict_types=1);
// 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_Options_Manager {
/** 管理中的選項設定 */
private static array $redirected_options = array();
/** 已知可安全 defer autoload 的選項名稱清單 */
private const KNOWN_DEFERRABLE_OPTIONS = array(
// 大型設定類(不需頁面載入即時讀取)
'wpseo_titles',
'wpseo_social',
'wpseo_internallinks',
'wpseo_xml',
'wpseo_flush_rewrite',
// WooCommerce 管理端設定
'woocommerce_tax_classes',
'woocommerce_shipping_debug_mode',
'woocommerce_schema_version',
// 其他常見巨型選項
'rewrite_rules',
'_transient_doing_cron',
'cron',
// 備份/匯出類
'updraft_backup_history',
'wpb_backup_options',
);
/** autoload 合計閾值(MB),超過則警告 */
private const AUTOLOAD_WARNING_THRESHOLD_MB = 1;
/** 單一選項大小閾值(bytes) */
private const SINGLE_OPTION_SIZE_THRESHOLD = 51200; // 50KB
public static function init(): void {
// 提供第三方登錄 API
/**
* 讓第三方外掛註冊要管理的選項
* add_action('wpdo_register_option_groups', function() {...});
*/
do_action( 'wpdo_register_option_groups' );
}
// ─────────────────────────────────────────────────────────
// Autoload 分析與最佳化
// ─────────────────────────────────────────────────────────
/**
* 分析當前 autoload 狀況
*/
public static function analyze_autoload(): array {
global $wpdb;
// 總體 autoload 大小
$total = $wpdb->get_row(
"
SELECT
COUNT(*) as cnt,
SUM(LENGTH(option_value)) as total_bytes
FROM {$wpdb->options}
WHERE autoload = 'yes'
",
ARRAY_A
);
// 最大的 50 個 autoload 選項
$largest = $wpdb->get_results(
"
SELECT
option_name,
LENGTH(option_value) AS size_bytes,
autoload
FROM {$wpdb->options}
WHERE autoload = 'yes'
ORDER BY size_bytes DESC
LIMIT 50
",
ARRAY_A
);
// 可 defer 的候選項
$deferrable_candidates = array();
foreach ( $largest as $row ) {
if ( (int) $row['size_bytes'] > self::SINGLE_OPTION_SIZE_THRESHOLD ||
in_array( $row['option_name'], self::KNOWN_DEFERRABLE_OPTIONS, true )
) {
$deferrable_candidates[] = $row;
}
}
return array(
'total_count' => (int) $total['cnt'],
'total_bytes' => (int) $total['total_bytes'],
'total_mb' => round( $total['total_bytes'] / 1024 / 1024, 2 ),
'warning' => (int) $total['total_bytes'] > self::AUTOLOAD_WARNING_THRESHOLD_MB * 1024 * 1024,
'largest' => $largest,
'deferrable' => $deferrable_candidates,
'estimated_save_mb' => round(
array_sum( array_column( $deferrable_candidates, 'size_bytes' ) ) / 1024 / 1024,
2
),
);
}
/**
* 執行 autoload 最佳化
*
* @param array $options_to_defer 要設為 autoload=no 的選項名稱陣列
* 若為空,使用內建安全清單
* @param bool $dry_run
*/
public static function optimize_autoload( array $options_to_defer = array(), bool $dry_run = false ): array {
global $wpdb;
if ( empty( $options_to_defer ) ) {
$options_to_defer = self::KNOWN_DEFERRABLE_OPTIONS;
}
$result = array(
'dry_run' => $dry_run,
'processed' => 0,
'saved_bytes' => 0,
'details' => array(),
);
foreach ( $options_to_defer as $option_name ) {
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT option_id, LENGTH(option_value) AS size_bytes, autoload
FROM {$wpdb->options}
WHERE option_name = %s",
$option_name
),
ARRAY_A
);
if ( ! $row ) {
continue;
}
if ( $row['autoload'] === 'no' ) {
continue;
}
if ( ! $dry_run ) {
$wpdb->update(
$wpdb->options,
array( 'autoload' => 'no' ),
array( 'option_name' => $option_name ),
array( '%s' ),
array( '%s' )
);
// 清除該選項的快取(下次讀取時會重建)
wp_cache_delete( $option_name, 'options' );
wp_cache_delete( 'alloptions', 'options' );
}
++$result['processed'];
$result['saved_bytes'] += (int) $row['size_bytes'];
$result['details'][] = array(
'option_name' => $option_name,
'saved_bytes' => (int) $row['size_bytes'],
);
}
$result['saved_mb'] = round( $result['saved_bytes'] / 1024 / 1024, 2 );
return $result;
}
// ─────────────────────────────────────────────────────────
// 選項群組重導向
// ─────────────────────────────────────────────────────────
/**
* 將一組選項重導向至專屬設定表
*
* @param string $group_name 設定群組名
* @param array $option_keys 要管理的 option_name 清單
*/
public static function register_settings_group( string $group_name, array $option_keys ): void {
global $wpdb;
$table = $wpdb->prefix . TMDO_TABLE_PREFIX . 'settings_' . sanitize_key( $group_name );
// 建立設定專屬表
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table} (
id INT NOT NULL AUTO_INCREMENT,
setting_key VARCHAR(191) NOT NULL,
setting_value LONGTEXT,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_key (setting_key)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
// 註冊為管理中選項
foreach ( $option_keys as $key ) {
self::$redirected_options[ $key ] = array(
'group' => $group_name,
'table' => $table,
);
// 攔截讀取
add_filter(
"pre_option_{$key}",
function ( $value ) use ( $key, $table ) {
return self::read_setting( $table, $key, $value );
},
10,
1
);
// 攔截寫入:寫入 UAE 表,並回傳 $old_value 使 WP 跳過寫 wp_options
add_filter(
"pre_update_option_{$key}",
function ( $value, $old_value ) use ( $key, $table ) {
self::write_setting( $table, $key, $value );
// 回傳 $old_value 會讓 update_option() 判定「值未變動」進而跳過 wp_options 寫入
return $old_value;
},
10,
2
);
}
}
private static function read_setting( string $table, string $key, $default ) {
global $wpdb;
$value = $wpdb->get_var(
$wpdb->prepare(
"SELECT setting_value FROM `{$table}` WHERE setting_key = %s",
$key
)
);
if ( $value === null ) {
return $default;
}
// v2.13.3: object-injection-safe unserialize (fixes L-DESER-1).
$decoded = TMDO_Safe_Unserialize::run( $value );
return $decoded;
}
private static function write_setting( string $table, string $key, $value ): void {
global $wpdb;
$wpdb->replace(
$table,
array(
'setting_key' => $key,
'setting_value' => maybe_serialize( $value ),
),
array( '%s', '%s' )
);
}
public static function get_redirected_options(): array {
return self::$redirected_options;
}
}