Files
2meet-data-optimizer/includes/class-tmdo-feature-flags.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

349 lines
8.8 KiB
PHP

<?php
/**
* Feature flags manager for WPDO module lifecycle states.
*
* @package WP_Data_Optimizer
*/
declare(strict_types=1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Feature flags for each WPDO module.
*
* Option key: wpdo_features (serialized array)
*
* 7-state lifecycle per module (extends HPCT's 4-state model):
* idle → dual_write → backfill → verify → cutover → cleanup → complete
*
* Any state can roll back to idle.
*/
class TMDO_Feature_Flags {
/** Option name storing all module states. */
private const OPTION_KEY = 'wpdo_features';
/**
* All known HPCT-inherited modules.
* These modules operate on existing hpct_* tables.
*/
public const HPCT_MODULES = array(
'reviews',
'messages',
'favorites',
'memberships',
'statistics',
'requests',
'listing_meta',
'wc_orders',
'latepoint',
);
/**
* Zone-based modules (new in WPDO).
* These modules operate on wpdo_* tables.
*/
public const ZONE_MODULES = array(
'hot_hp_listing',
'hot_hp_vendor',
'warm',
'cold_hp_listing',
'cold_hp_vendor',
'archive',
);
/** Valid lifecycle states. */
public const VALID_STATES = array(
'idle',
'dual_write',
'backfill',
'verify',
'cutover',
'cleanup',
'complete',
);
/**
* States in which interceptors should capture writes (dual-write active).
*/
public const WRITE_ACTIVE_STATES = array(
'dual_write',
'backfill',
'verify',
'cutover',
);
/**
* States in which reads come from the custom/zone table.
*/
public const READ_CUSTOM_STATES = array(
'cutover',
'cleanup',
'complete',
);
/**
* States in which query interceptors are active.
*/
public const QUERY_ACTIVE_STATES = array(
'cutover',
'cleanup',
'complete',
);
/**
* Get the state of a single module.
*
* @param string $module Module name.
* @return string One of VALID_STATES, defaults to 'idle'.
*/
public static function get( string $module ): string {
$flags = self::all();
return $flags[ $module ] ?? 'idle';
}
/**
* Set the state of a module.
*
* V2.2.0 M2: gated by TMDO_FSM_Guard — invalid transitions are blocked
* (returns false), destructive transitions automatically snapshot first.
* Filter `wpdo/fsm_guard/bypass` allows CLI/tests to override.
*
* @param string $module Module name.
* @param string $state One of VALID_STATES.
* @return bool|WP_Error true on success, WP_Error on guard rejection,
* false on invalid state name.
*/
public static function set( string $module, string $state ) {
if ( ! in_array( $state, self::VALID_STATES, true ) ) {
return false;
}
// FSM Guard: validate transition + auto-snapshot if destructive.
// Defensive: only invoke when class is loaded AND not explicitly
// disabled (raw-PHP unit harness sets TMDO_FSM_GUARD_DISABLED).
// Dedicated FSMGuardTest unsets the constant before its own assertions.
$guard_active = class_exists( 'TMDO_FSM_Guard' )
&& ! ( defined( 'TMDO_FSM_GUARD_DISABLED' ) && TMDO_FSM_GUARD_DISABLED );
if ( $guard_active ) {
$check = TMDO_FSM_Guard::before_transition( $module, $state );
if ( is_wp_error( $check ) ) {
return $check;
}
}
$flags = self::all();
$flags[ $module ] = $state;
update_option( self::OPTION_KEY, $flags );
self::$cache = null; // Invalidate request-level cache.
// Record entry timestamp (M2) for downstream verify-gate / wash-period checks.
if ( $guard_active ) {
TMDO_FSM_Guard::record_entry( $module, $state );
}
return true;
}
// ── shadow_read_only sub-flag (PR-4) ────────────────────────────────────
// Orthogonal to the main 7-state FSM. When enabled for a module that is in
// the `verify` state, every read fans out to BOTH postmeta and the zone
// table; divergence is logged to wp_wpdo_shadow_diffs without affecting
// the served value. Lets ops bake confidence before cutover.
/** Option key storing per-module shadow_read_only flags. */
private const SHADOW_OPTION_KEY = 'wpdo_features_shadow';
/**
* Request-level cache for shadow flags.
*
* @var array<string,bool>|null
*/
private static ?array $shadow_cache = null;
/**
* Enable shadow_read_only for a module (only meaningful in the verify state).
*
* @param string $module Module identifier.
* @return bool True on success.
*/
public static function enable_shadow_read( string $module ): bool {
$flags = self::all_shadow();
$flags[ $module ] = true;
update_option( self::SHADOW_OPTION_KEY, $flags, false );
self::$shadow_cache = null;
return true;
}
/**
* Disable shadow_read_only for a module.
*
* @param string $module Module identifier.
* @return bool
*/
public static function disable_shadow_read( string $module ): bool {
$flags = self::all_shadow();
unset( $flags[ $module ] );
update_option( self::SHADOW_OPTION_KEY, $flags, false );
self::$shadow_cache = null;
return true;
}
/**
* Whether shadow_read_only is currently active for a module.
*
* Returns true only when:
* - Module is in the `verify` state, AND
* - Shadow flag has been explicitly enabled.
*
* @param string $module Module identifier.
* @return bool
*/
public static function is_shadow_read_active( string $module ): bool {
if ( 'verify' !== self::get( $module ) ) {
return false;
}
$flags = self::all_shadow();
return ! empty( $flags[ $module ] );
}
/**
* Return all shadow flags.
*
* @return array<string,bool>
*/
public static function all_shadow(): array {
if ( null !== self::$shadow_cache ) {
return self::$shadow_cache;
}
$saved = get_option( self::SHADOW_OPTION_KEY, array() );
if ( ! is_array( $saved ) ) {
$saved = array();
}
self::$shadow_cache = $saved;
return $saved;
}
/**
* Check if a module is fully complete (reads and writes on custom table only).
*
* @param string $module Module identifier.
* @return bool True if module state is 'complete'.
*/
public static function is_complete( string $module ): bool {
return 'complete' === self::get( $module );
}
/**
* Check if dual-write is active for a module.
*
* @param string $module Module identifier.
* @return bool True if module is in a write-active state.
*/
public static function is_write_active( string $module ): bool {
return in_array( self::get( $module ), self::WRITE_ACTIVE_STATES, true );
}
/**
* Check if reads should come from the custom/zone table.
*
* @param string $module Module identifier.
* @return bool True if module is in a read-custom state.
*/
public static function is_read_custom( string $module ): bool {
return in_array( self::get( $module ), self::READ_CUSTOM_STATES, true );
}
/**
* Check if query interceptors should be active.
*
* @param string $module Module identifier.
* @return bool True if module is in a query-active state.
*/
public static function is_query_active( string $module ): bool {
return in_array( self::get( $module ), self::QUERY_ACTIVE_STATES, true );
}
/**
* Reset a module to idle (rollback).
*
* @param string $module Module identifier.
* @return void
*/
public static function reset( string $module ): void {
self::set( $module, 'idle' );
self::$cache = null; // Invalidate request-level cache.
}
/**
* Request-level cache for all() to avoid repeated get_option() calls.
*
* @var array|null
*/
private static ?array $cache = null;
/**
* Return all module states.
*
* @return array<string, string>
*/
public static function all(): array {
if ( null !== self::$cache ) {
return self::$cache;
}
$saved = get_option( self::OPTION_KEY, array() );
if ( ! is_array( $saved ) ) {
$saved = array();
}
$all_modules = array_merge( self::HPCT_MODULES, self::ZONE_MODULES );
$defaults = array_fill_keys( $all_modules, 'idle' );
self::$cache = array_merge( $defaults, $saved );
return self::$cache;
}
/**
* Return only HPCT-inherited module states.
*
* @return array<string, string>
*/
public static function hpct_modules(): array {
$all = self::all();
return array_intersect_key( $all, array_flip( self::HPCT_MODULES ) );
}
/**
* Return only zone module states.
*
* @return array<string, string>
*/
public static function zone_modules(): array {
$all = self::all();
return array_intersect_key( $all, array_flip( self::ZONE_MODULES ) );
}
/**
* Map an HPCT feature flag status to a WPDO state.
*
* Used during HPCT import to translate HPCT's 4-state model
* to WPDO's 7-state model.
*
* @param string $hpct_status HPCT status (disabled/migrating/verified/enabled).
* @return string WPDO state.
*/
public static function map_hpct_status( string $hpct_status ): string {
return match ( $hpct_status ) {
'disabled' => 'idle',
'migrating' => 'backfill',
'verified' => 'cutover',
'enabled' => 'complete',
default => 'idle',
};
}
}