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
286 lines
8.1 KiB
PHP
286 lines
8.1 KiB
PHP
<?php
|
|
/**
|
|
* TMDO_Options_Manager - wp_options 反 EAV 模組
|
|
*
|
|
* 功能:
|
|
* - autoload 最佳化(掃描並 defer 過大的 autoload 項目)
|
|
* - 選項重導向(將高頻選項搬至專屬設定表)
|
|
* - 快取攔截
|
|
*
|
|
* @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_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;
|
|
}
|
|
}
|