Files
2meet-data-optimizer/includes/engine/class-tmdo-auto-promoter.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

136 lines
4.8 KiB
PHP

<?php
/**
* TMDO_Auto_Promoter — shadow_read 穩定期後自動升級到 aeav_only。
*
* 僅針對 `shadow_read` 做自動升級(升級到 aeav_only),其他 mode 不自動動。
* 判斷條件(皆滿足才觸發):
* 1. 當前 mode = shadow_read
* 2. 進入 shadow_read 距今 ≥ `min_days`
* 3. 進入後 `shadow_diffs` 表中該 entity 的 diff 筆數 = 0
*
* 這符合「穩定期沒問題就進 production」的安全遷移原則。
* 若 diff 出現,規則永不觸發;人工介入即可。
*
* 可由 option 或 filter 調整:
* option `wpdo_auto_promote_enabled` — 全域開關(bool,預設 false)
* option `wpdo_auto_promote_min_days` — 停留天數門檻(int,預設 7)
* filter `wpdo_auto_promote_should_run` — per-entity 覆寫(bool default)
*
* @package WP_Data_Optimizer
* @since 1.5.0
*/
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_Auto_Promoter {
public const OPT_ENABLED = 'wpdo_auto_promote_enabled';
public const OPT_MIN_DAYS = 'wpdo_auto_promote_min_days';
public const DEFAULT_MIN_DAYS = 7;
public const CRON_HOOK = 'wpdo_auto_promote_check';
public static function init(): void {
add_action( self::CRON_HOOK, array( self::class, 'check_all' ) );
add_action( 'init', array( self::class, 'maybe_schedule' ), 30 );
}
/**
* 首次啟用時排入 daily cron。Option 關閉時會停掉,開啟時重排。
*/
public static function maybe_schedule(): void {
$enabled = (bool) get_option( self::OPT_ENABLED, false );
$scheduled = wp_next_scheduled( self::CRON_HOOK );
if ( $enabled && ! $scheduled ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK );
} elseif ( ! $enabled && $scheduled ) {
wp_unschedule_event( $scheduled, self::CRON_HOOK );
}
}
/**
* 遍歷所有 entity,符合條件者升級為 aeav_only。
*
* @return array<string, string> entity_type → 結果('promoted'|'not_ready'|'no_diff_yet'|'disabled_globally')
*/
public static function check_all(): array {
$results = array();
if ( ! (bool) get_option( self::OPT_ENABLED, false ) ) {
foreach ( array( 'post', 'user', 'term', 'comment' ) as $type ) {
$results[ $type ] = 'disabled_globally';
}
return $results;
}
$min_days = (int) get_option( self::OPT_MIN_DAYS, self::DEFAULT_MIN_DAYS );
$entered_all = get_option( TMDO_Mode_Manager::OPT_ENTERED_AT, array() );
foreach ( array( 'post', 'user', 'term', 'comment' ) as $type ) {
$results[ $type ] = self::check_entity( $type, $min_days, $entered_all );
}
return $results;
}
/**
* 評估單一 entity 是否可升級。
*
* @return string 'promoted' | 'not_shadow_read' | 'not_ready' | 'has_diff' | 'vetoed'
*/
public static function check_entity( string $type, int $min_days, array $entered_all ): string {
$current = TMDO_Mode_Manager::get( $type );
if ( $current !== TMDO_Mode_Manager::MODE_SHADOW_READ ) {
return 'not_shadow_read';
}
$entered_at = isset( $entered_all[ $type ] ) ? (int) $entered_all[ $type ] : 0;
if ( $entered_at === 0 || ( time() - $entered_at ) < $min_days * DAY_IN_SECONDS ) {
return 'not_ready';
}
$diff_counts = TMDO_Shadow_Diff_Logger::count_by_entity();
$count = (int) ( $diff_counts[ $type ] ?? 0 );
if ( $count > 0 ) {
return 'has_diff';
}
// 讓外部 filter 最後一次 veto(例如:還在外部驗證期間)
$should = apply_filters( 'wpdo_auto_promote_should_run', true, $type, $entered_at, $count );
if ( ! $should ) {
return 'vetoed';
}
$result = TMDO_Mode_Manager::set( $type, TMDO_Mode_Manager::MODE_AEAV_ONLY );
if ( is_wp_error( $result ) ) {
TMDO_Logger::error(
'auto_promoter',
'auto_promote_failed',
$result->get_error_message(),
array( 'entity_type' => $type )
);
return 'error';
}
TMDO_Logger::info(
'auto_promote_success',
array(
'entity_type' => $type,
'entered_at' => gmdate( 'c', $entered_at ),
'days_elapsed' => (int) ( ( time() - $entered_at ) / DAY_IN_SECONDS ),
'min_days' => $min_days,
)
);
do_action( 'wpdo_auto_promoted', $type, $entered_at, $min_days );
return 'promoted';
}
}