*/ private const OPT_KEY = 'wpdo_bridge_modes'; /** * Per-entity 進入當前 mode 的 unix timestamp(v1.5.0+ 供 auto-promoter 使用)。 * Shape: array */ public const OPT_ENTERED_AT = 'wpdo_bridge_mode_entered_at'; /** Per-request memoization(避免每次 filter 都查 DB option) */ private static ?array $cache = null; /** * 預設模式(v2.5.4 起): * post → disabled(由 Legacy Feature_Flags FSM 管理,不由 Mode_Manager 控制) * user / term / comment → dual_write(安全雙寫:寫入 flat table + 原生 EAV,讀取仍走 EAV) */ private static function defaults(): array { return array( 'post' => self::MODE_DISABLED, 'user' => self::MODE_DUAL_WRITE, 'term' => self::MODE_DUAL_WRITE, 'comment' => self::MODE_DUAL_WRITE, ); } /** * 取得所有 entity 的 mode * * @return array */ public static function all(): array { if ( self::$cache !== null ) { return self::$cache; } $stored = get_option( self::OPT_KEY, array() ); $modes = self::defaults(); if ( is_array( $stored ) ) { foreach ( $stored as $type => $mode ) { if ( is_string( $type ) && self::is_valid_mode( $mode ) && in_array( $type, TMDO_Entity_Registry::VALID_ENTITY_TYPES, true ) ) { $modes[ $type ] = $mode; } } } self::$cache = $modes; return $modes; } /** * 取得單一 entity type 的 mode */ public static function get( string $entity_type ): string { $all = self::all(); return $all[ $entity_type ] ?? self::MODE_DISABLED; } /** * 設定單一 entity type 的 mode(包含轉換安全檢查) * * @return true|\WP_Error */ public static function set( string $entity_type, string $new_mode ) { if ( ! in_array( $entity_type, TMDO_Entity_Registry::VALID_ENTITY_TYPES, true ) ) { return new \WP_Error( 'invalid_entity', "Invalid entity type: {$entity_type}" ); } if ( ! self::is_valid_mode( $new_mode ) ) { return new \WP_Error( 'invalid_mode', "Invalid mode: {$new_mode}" ); } $current = self::get( $entity_type ); if ( $current === $new_mode ) { return true; // no-op } // 驗證轉換安全性 if ( ! self::is_safe_transition( $current, $new_mode ) ) { return new \WP_Error( 'unsafe_transition', sprintf( __( '不安全的模式轉換:%1$s → %2$s。建議路徑:disabled → dual_write → shadow_read → aeav_only', '2meet-data-optimizer' ), $current, $new_mode ) ); } $all = self::all(); $all[ $entity_type ] = $new_mode; update_option( self::OPT_KEY, $all, false ); self::$cache = $all; // 記錄進入此 mode 的 timestamp — auto-promoter 用來判斷停留天數。 $entered = get_option( self::OPT_ENTERED_AT, array() ); if ( ! is_array( $entered ) ) { $entered = array(); } $entered[ $entity_type ] = time(); update_option( self::OPT_ENTERED_AT, $entered, false ); TMDO_Logger::info( 'bridge_mode_changed', array( 'entity_type' => $entity_type, 'from' => $current, 'to' => $new_mode, 'user_id' => get_current_user_id(), ) ); // 模式變更時清除所有相關快取 TMDO_Cache_Orchestrator::flush_entity( $entity_type ); do_action( 'wpdo_bridge_mode_changed', $entity_type, $new_mode, $current ); return true; } /** * 緊急 kill-switch:全部 entity 瞬間切到 disabled * * 這是**安全轉換規則的例外** — 任何狀態都可以瞬間降到 disabled, * 因為 EAV 資料一直存在(dual_write / shadow_read 都還寫 EAV), * 只有 aeav_only 切到 disabled 才有資料遺失風險。 * * 呼叫這個方法表示「出事了,先退回安全狀態」,EAV 可能不是最新的, * 但至少系統不會壞。 * * @return int 改變狀態的 entity 數量 */ public static function emergency_disable_all(): int { $all = self::all(); $changed = 0; foreach ( $all as $type => $mode ) { if ( $mode !== self::MODE_DISABLED ) { $all[ $type ] = self::MODE_DISABLED; ++$changed; TMDO_Logger::warning( 'bridge_emergency_disable', array( 'entity_type' => $type, 'previous_mode' => $mode, 'user_id' => get_current_user_id(), 'is_aeav_only' => $mode === self::MODE_AEAV_ONLY, 'warning' => $mode === self::MODE_AEAV_ONLY ? 'CRITICAL: switching from aeav_only to disabled — EAV may be stale' : 'Normal emergency fallback', ) ); } } if ( $changed > 0 ) { update_option( self::OPT_KEY, $all, false ); self::$cache = $all; TMDO_Cache_Orchestrator::flush_all(); do_action( 'wpdo_bridge_emergency_disabled', $changed ); } return $changed; } /** * 設定全部 entity 到同一個 mode(批次操作,會做轉換檢查) * * @return array 逐 entity 的結果 */ public static function set_all( string $mode ): array { $results = array(); foreach ( TMDO_Entity_Registry::VALID_ENTITY_TYPES as $type ) { $results[ $type ] = self::set( $type, $mode ); } return $results; } /** * Helper: 目前 mode 是否會寫入 flat table? */ public static function writes_to_flat( string $entity_type ): bool { return in_array( self::get( $entity_type ), array( self::MODE_DUAL_WRITE, self::MODE_SHADOW_READ, self::MODE_AEAV_ONLY, ), true ); } /** * Helper: 目前 mode 是否會讓 WP 原生 EAV 也被寫入? */ public static function writes_to_eav( string $entity_type ): bool { return in_array( self::get( $entity_type ), array( self::MODE_DISABLED, self::MODE_DUAL_WRITE, self::MODE_SHADOW_READ, ), true ); } /** * Helper: 目前 mode 是否從 flat table 讀取? */ public static function reads_from_flat( string $entity_type ): bool { return in_array( self::get( $entity_type ), array( self::MODE_SHADOW_READ, self::MODE_AEAV_ONLY, ), true ); } /** * Helper: 是否要做 shadow 比對? */ public static function does_shadow_compare( string $entity_type ): bool { return self::get( $entity_type ) === self::MODE_SHADOW_READ; } // ───────────────────────────────────────────────────────── // 驗證 // ───────────────────────────────────────────────────────── public static function is_valid_mode( $mode ): bool { return is_string( $mode ) && in_array( $mode, self::ALL_MODES, true ); } /** * 轉換是否安全? * * 規則: * - 相鄰的階梯移動:允許(正向或反向) * - 跨階梯降級:允許(後面的 fallback,EAV 還在) * - 跨階梯升級:禁止(下游資料可能還沒跟上) * * 階梯順序(index 越大 = 越「靠 UAE」): * 0: disabled * 1: dual_write * 2: shadow_read * 3: aeav_only * * 降級(index 變小)永遠安全(EAV 都還在,除了 aeav_only→disabled 一跳, * 但那是刻意的 emergency 用法 — 見 emergency_disable_all)。 * * 升級只允許 +1(disabled→dual_write、dual_write→shadow_read、shadow_read→aeav_only)。 */ public static function is_safe_transition( string $from, string $to ): bool { $order = array( self::MODE_DISABLED => 0, self::MODE_DUAL_WRITE => 1, self::MODE_SHADOW_READ => 2, self::MODE_AEAV_ONLY => 3, ); if ( ! isset( $order[ $from ], $order[ $to ] ) ) { return false; } $diff = $order[ $to ] - $order[ $from ]; // 升級:只允許 +1 if ( $diff > 0 ) { return $diff === 1; } // 降級:一律允許(這是 fallback,資料都還在) return true; } // ───────────────────────────────────────────────────────── // 內部 // ───────────────────────────────────────────────────────── /** * 測試用:清除 memoization */ public static function reset_cache(): void { self::$cache = null; } /** * Human-readable label */ public static function label( string $mode ): string { switch ( $mode ) { case self::MODE_DISABLED: return __( '停用(原生 EAV)', '2meet-data-optimizer' ); case self::MODE_DUAL_WRITE: return __( '雙寫', '2meet-data-optimizer' ); case self::MODE_SHADOW_READ: return __( '影子讀取(驗證中)', '2meet-data-optimizer' ); case self::MODE_AEAV_ONLY: return __( '僅 UAE(生產模式)', '2meet-data-optimizer' ); default: return $mode; } } /** * Mode 的說明文字(給 admin UI 用) */ public static function description( string $mode ): string { switch ( $mode ) { case self::MODE_DISABLED: return __( '完全停用 UAE 攔截,使用 WordPress 原生 wp_*meta 表。安全 fallback。', '2meet-data-optimizer' ); case self::MODE_DUAL_WRITE: return __( '寫入 UAE + 原生 meta 表,讀取仍走原生。遷移前期的安全起點。', '2meet-data-optimizer' ); case self::MODE_SHADOW_READ: return __( '寫入雙寫,讀取走 UAE 並比對原生 meta。驗證資料一致性的階段。', '2meet-data-optimizer' ); case self::MODE_AEAV_ONLY: return __( '僅讀寫 UAE,不再寫入原生 meta 表。最終生產模式,效能最佳。', '2meet-data-optimizer' ); default: return ''; } } }