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

332 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* TMDO_Entity_Health — Entity Bridge 健康狀態聚合器
*
* 收集每個 entity type (user/term/comment) 的即時健康快照:
* - 模式與已停留天數
* - 每個群組的覆蓋率(flat table rows vs EAV rows
* - Shadow diff 計數
* - 遷移進度(斷點位置、已搬移筆數)
* - Auto-promote 資格評估
* - 操作建議
*
* @package WP_Data_Optimizer
* @since 2.6.6
*/
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,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions -- inherits engine coding standard.
defined( 'ABSPATH' ) || exit;
final class TMDO_Entity_Health {
/** Entity types managed by this class (post is handled by Feature_Flags FSM). */
public const MANAGED_TYPES = array( 'user', 'term', 'comment' );
/**
* 取得全部 entity 的健康快照(批次,供 REST polling 用)。
*
* @return array<string, array>
*/
public static function get_all(): array {
$result = array();
foreach ( self::MANAGED_TYPES as $type ) {
$result[ $type ] = self::get_one( $type );
}
return $result;
}
/**
* 取得單一 entity 的健康快照。
*/
public static function get_one( string $entity_type ): array {
$mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( $entity_type ) : 'disabled';
$entered_all = get_option( TMDO_Mode_Manager::OPT_ENTERED_AT, array() );
$entered_at = isset( $entered_all[ $entity_type ] ) ? (int) $entered_all[ $entity_type ] : 0;
$mode_days = $entered_at > 0 ? (int) floor( ( time() - $entered_at ) / DAY_IN_SECONDS ) : 0;
$groups = self::get_groups_health( $entity_type );
$shadow_diffs = 0;
if ( class_exists( 'TMDO_Shadow_Diff_Logger' ) ) {
$diff_counts = TMDO_Shadow_Diff_Logger::count_by_entity();
$shadow_diffs = (int) ( $diff_counts[ $entity_type ] ?? 0 );
}
$auto_promote = self::get_auto_promote_status( $entity_type, $mode, $entered_at, $shadow_diffs );
$backfill_active = self::is_backfill_active( $entity_type );
$native_counts = self::get_native_counts( $entity_type );
return array(
'entity_type' => $entity_type,
'mode' => $mode,
'entered_at' => $entered_at,
'mode_days' => $mode_days,
'groups' => $groups,
'shadow_diffs' => $shadow_diffs,
'auto_promote' => $auto_promote,
'backfill_active' => $backfill_active,
'native_entity_count' => $native_counts['entity_count'],
'native_meta_count' => $native_counts['meta_count'],
'native_entity_table' => $native_counts['entity_table'],
'native_meta_table' => $native_counts['meta_table'],
'recommendation' => self::get_recommendation( $entity_type, $mode, $groups, $shadow_diffs ),
'next_mode' => self::get_next_mode( $mode ),
'prev_mode' => self::get_prev_mode( $mode ),
);
}
/**
* 取得 native EAV 表的總筆數(entity 本體 + meta)。
*/
private static function get_native_counts( string $entity_type ): array {
global $wpdb;
$adapter = class_exists( 'TMDO_Entity_Registry' ) ? TMDO_Entity_Registry::get_adapter( $entity_type ) : null;
if ( ! $adapter ) {
return array(
'entity_table' => '',
'meta_table' => '',
'entity_count' => 0,
'meta_count' => 0,
);
}
$meta_table = $adapter->get_native_meta_table();
$entity_table = self::get_native_entity_table( $entity_type );
$entity_count = $entity_table ? (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$entity_table}`" ) : 0;
$meta_count = $meta_table ? (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$meta_table}`" ) : 0;
return array(
'entity_table' => $entity_table,
'meta_table' => $meta_table,
'entity_count' => $entity_count,
'meta_count' => $meta_count,
);
}
/**
* 取得 entity 本體資料表名稱(非 meta 表)。
*/
private static function get_native_entity_table( string $entity_type ): string {
global $wpdb;
$map = array(
'user' => $wpdb->users,
'term' => $wpdb->terms,
'comment' => $wpdb->comments,
'post' => $wpdb->posts,
);
return $map[ $entity_type ] ?? '';
}
// ─────────────────────────────────────────────────────────
// 群組覆蓋率
// ─────────────────────────────────────────────────────────
private static function get_groups_health( string $entity_type ): array {
if ( ! class_exists( 'TMDO_Entity_Registry' ) || ! class_exists( 'TMDO_Schema_Manager' ) ) {
return array();
}
global $wpdb;
$groups = TMDO_Entity_Registry::get_groups_for_type( $entity_type );
$adapter = TMDO_Entity_Registry::get_adapter( $entity_type );
$result = array();
foreach ( $groups as $group_name ) {
$fields = TMDO_Entity_Registry::get_group_fields( $entity_type, $group_name );
$table = TMDO_Schema_Manager::get_table_name( $entity_type, $group_name );
$table_exists = TMDO_Schema_Manager::table_exists( $table );
$flat_rows = 0;
$eav_rows = 0;
if ( $table_exists && $adapter ) {
$flat_rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
$managed_keys = array_column( $fields, 'key' );
if ( ! empty( $managed_keys ) ) {
$meta_table = $adapter->get_native_meta_table();
$id_col = $adapter->get_entity_id_column();
$phs = implode( ',', array_fill( 0, count( $managed_keys ), '%s' ) );
$eav_rows = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT `{$id_col}`) FROM `{$meta_table}` WHERE `meta_key` IN ({$phs})",
...$managed_keys
)
);
}
}
$coverage_pct = 0.0;
if ( $eav_rows > 0 ) {
$coverage_pct = round( min( $flat_rows / $eav_rows, 1.0 ) * 100, 1 );
} elseif ( $flat_rows > 0 ) {
$coverage_pct = 100.0;
}
$migration_info = self::get_migration_status( $entity_type, $group_name );
$result[] = array(
'name' => $group_name,
'fields_count' => count( $fields ),
'table' => $table,
'table_exists' => $table_exists,
'flat_rows' => $flat_rows,
'eav_rows' => $eav_rows,
'coverage_pct' => $coverage_pct,
'migration_status' => $migration_info['status'],
'last_id' => $migration_info['last_id'],
'total_migrated' => $migration_info['total_migrated'],
'started_at' => $migration_info['started_at'],
'completed_at' => $migration_info['completed_at'],
);
}
return $result;
}
private static function get_migration_status( string $entity_type, string $group_name ): array {
global $wpdb;
$table = $wpdb->prefix . TMDO_TABLE_PREFIX . 'migration_status';
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT status, last_id, total_migrated, started_at, completed_at FROM `{$table}` WHERE entity_type = %s AND group_name = %s",
$entity_type,
$group_name
),
ARRAY_A
);
return array(
'status' => $row['status'] ?? 'not_started',
'last_id' => (int) ( $row['last_id'] ?? 0 ),
'total_migrated' => (int) ( $row['total_migrated'] ?? 0 ),
'started_at' => $row['started_at'] ?? null,
'completed_at' => $row['completed_at'] ?? null,
);
}
// ─────────────────────────────────────────────────────────
// Auto-promote 資格評估
// ─────────────────────────────────────────────────────────
private static function get_auto_promote_status(
string $entity_type,
string $mode,
int $entered_at,
int $shadow_diffs
): array {
$enabled = (bool) get_option( TMDO_Auto_Promoter::OPT_ENABLED, false );
$min_days = (int) get_option( TMDO_Auto_Promoter::OPT_MIN_DAYS, TMDO_Auto_Promoter::DEFAULT_MIN_DAYS );
$days_elapsed = $entered_at > 0 ? (int) floor( ( time() - $entered_at ) / DAY_IN_SECONDS ) : 0;
if ( $mode !== TMDO_Mode_Manager::MODE_SHADOW_READ ) {
return array(
'enabled' => $enabled,
'min_days' => $min_days,
'eligible' => false,
'reason' => 'not_shadow_read',
);
}
if ( $days_elapsed < $min_days ) {
return array(
'enabled' => $enabled,
'min_days' => $min_days,
'days_elapsed' => $days_elapsed,
'eligible' => false,
'reason' => "need_{$min_days}_days",
);
}
if ( $shadow_diffs > 0 ) {
return array(
'enabled' => $enabled,
'min_days' => $min_days,
'days_elapsed' => $days_elapsed,
'eligible' => false,
'reason' => "has_{$shadow_diffs}_diffs",
);
}
return array(
'enabled' => $enabled,
'min_days' => $min_days,
'days_elapsed' => $days_elapsed,
'eligible' => true,
'reason' => 'ready',
);
}
// ─────────────────────────────────────────────────────────
// 建議與模式轉換
// ─────────────────────────────────────────────────────────
private static function get_recommendation(
string $entity_type,
string $mode,
array $groups,
int $shadow_diffs
): string {
switch ( $mode ) {
case TMDO_Mode_Manager::MODE_DISABLED:
return '啟用 Hook Bus 並切換到 dual_write,讓系統開始對 flat table 雙寫。';
case TMDO_Mode_Manager::MODE_DUAL_WRITE:
foreach ( $groups as $g ) {
if ( $g['coverage_pct'] < 99.0 ) {
return '執行 Backfill 遷移歷史資料,待所有群組覆蓋率達到 100% 後再升級到 shadow_read。';
}
}
return '所有群組覆蓋率已達 100%,可以升級到 shadow_read 進行驗證期觀察。';
case TMDO_Mode_Manager::MODE_SHADOW_READ:
if ( $shadow_diffs > 0 ) {
return "發現 {$shadow_diffs} 筆 shadow diff,請先調查差異原因(Settings → Shadow Diffs)後再考慮升級。";
}
$min_days = (int) get_option( TMDO_Auto_Promoter::OPT_MIN_DAYS, TMDO_Auto_Promoter::DEFAULT_MIN_DAYS );
$entered_all = get_option( TMDO_Mode_Manager::OPT_ENTERED_AT, array() );
$entered_at = isset( $entered_all[ $entity_type ] ) ? (int) $entered_all[ $entity_type ] : 0;
$days = $entered_at > 0 ? (int) floor( ( time() - $entered_at ) / DAY_IN_SECONDS ) : 0;
if ( $days < $min_days ) {
return "繼續觀察中(已 {$days}/{$min_days} 天)。無 diff 且穩定後可升級到 aeav_only。";
}
return '已達穩定期,可以升級到 aeav_only(最終生產模式)。';
case TMDO_Mode_Manager::MODE_AEAV_ONLY:
return '遷移完成。所有讀寫均走 flat table,效能最佳。';
default:
return '';
}
}
private static function get_next_mode( string $mode ): ?string {
$idx = array_search( $mode, TMDO_Mode_Manager::ALL_MODES, true );
if ( $idx === false || $idx >= count( TMDO_Mode_Manager::ALL_MODES ) - 1 ) {
return null;
}
return TMDO_Mode_Manager::ALL_MODES[ $idx + 1 ];
}
private static function get_prev_mode( string $mode ): ?string {
$idx = array_search( $mode, TMDO_Mode_Manager::ALL_MODES, true );
if ( $idx === false || $idx <= 0 ) {
return null;
}
return TMDO_Mode_Manager::ALL_MODES[ $idx - 1 ];
}
private static function is_backfill_active( string $entity_type ): bool {
return (bool) wp_next_scheduled( 'wpdo_entity_backfill_batch', array( $entity_type ) );
}
}