array( 'dual_write' ), 'dual_write' => array( 'backfill', 'idle' ), 'backfill' => array( 'verify', 'dual_write', 'idle' ), 'verify' => array( 'cutover', 'dual_write', 'idle' ), 'cutover' => array( 'cleanup', 'idle' ), 'cleanup' => array( 'complete', 'idle' ), 'complete' => array( 'idle' ), ); /** * Registry of migration class instances, keyed by module name. * * @var array */ private static array $migrations = array(); /** * Register a migration class for a module. * * @param string $module Module identifier. * @param TMDO_Migration_Base $migration Migration instance. * @return void */ public static function register( string $module, TMDO_Migration_Base $migration ): void { self::$migrations[ $module ] = $migration; } /** * Get the migration instance for a module. * * @param string $module Module identifier. * @return TMDO_Migration_Base|null Migration instance, or null if not found. */ public static function get_migration( string $module ): ?TMDO_Migration_Base { return self::$migrations[ $module ] ?? null; } /** * Get all registered migration instances. * * @return array */ public static function all(): array { return self::$migrations; } // ── State transitions ───────────────────────────────────────────────── /** * Check if a state transition is valid. * * @param string $module Module identifier. * @param string $target_state Target state to transition to. * @return bool True if the transition is allowed. */ public static function can_transition( string $module, string $target_state ): bool { $current = TMDO_Feature_Flags::get( $module ); // Rollback to idle is always allowed. if ( 'idle' === $target_state ) { return true; } $allowed = self::TRANSITIONS[ $current ] ?? array(); return in_array( $target_state, $allowed, true ); } /** * Transition a module to a new state. * * @param string $module Module identifier. * @param string $target_state Target state to transition to. * @return bool True on success, false if transition is invalid. */ public static function transition( string $module, string $target_state ): bool { if ( ! self::can_transition( $module, $target_state ) ) { return false; } return TMDO_Feature_Flags::set( $module, $target_state ); } // ── High-level operations ───────────────────────────────────────────── /** * Start or resume migration for a module. * * Flow: idle → dual_write → backfill (run batches) * * @param string $module Module name. * @param callable|null $progress_callback Called after each batch. * @return array{status: string, message: string} */ public static function migrate( string $module, ?callable $progress_callback = null ): array { $migration = self::get_migration( $module ); if ( ! $migration ) { return array( 'status' => 'error', 'message' => "No migration registered for module: {$module}", ); } $current = TMDO_Feature_Flags::get( $module ); // Already in backfill — resume. if ( 'backfill' === $current ) { $completed = $migration->run( true, $progress_callback ); return array( 'status' => $completed ? 'verify' : 'backfill', 'message' => $completed ? 'Backfill complete. Ready to verify.' : 'Backfill timed out. Resume to continue.', ); } // Already past backfill. if ( in_array( $current, array( 'verify', 'cutover', 'cleanup', 'complete' ), true ) ) { return array( 'status' => $current, 'message' => "Module is already in state: {$current}.", ); } // Start fresh: idle or dual_write → backfill. if ( ! self::transition( $module, 'dual_write' ) && 'dual_write' !== $current ) { return array( 'status' => 'error', 'message' => "Cannot start migration from state: {$current}", ); } $completed = $migration->run( false, $progress_callback ); return array( 'status' => $completed ? 'verify' : 'backfill', 'message' => $completed ? 'Backfill complete. Ready to verify.' : 'Backfill timed out. Resume to continue.', ); } /** * Verify data consistency for a module. * * @param string $module Module identifier. * @return array{status: string, message: string, verified: bool} Verification result. */ public static function verify( string $module ): array { $migration = self::get_migration( $module ); if ( ! $migration ) { return array( 'status' => 'error', 'message' => "No migration registered for module: {$module}", 'verified' => false, ); } $current = TMDO_Feature_Flags::get( $module ); if ( 'verify' !== $current ) { return array( 'status' => 'error', 'message' => "Module must be in 'verify' state. Current: {$current}", 'verified' => false, ); } $ok = $migration->verify_counts(); if ( ! $ok ) { // Verification failed — allow retry via dual_write → backfill. self::transition( $module, 'dual_write' ); return array( 'status' => 'dual_write', 'message' => 'Verification failed. Rolled back to dual_write for retry.', 'verified' => false, ); } return array( 'status' => 'verify', 'message' => 'Verification passed. Ready for cutover.', 'verified' => true, ); } /** * Cutover: switch reads to the custom table. * * @param string $module Module identifier. * @return array{status: string, message: string} Operation result. */ public static function cutover( string $module ): array { $current = TMDO_Feature_Flags::get( $module ); if ( 'verify' !== $current ) { return array( 'status' => 'error', 'message' => "Module must be in 'verify' state. Current: {$current}", ); } self::transition( $module, 'cutover' ); return array( 'status' => 'cutover', 'message' => 'Cutover complete. Reads now come from the custom table. Run cleanup when ready.', ); } /** * Rollback: return to idle state from any state. * * @param string $module Module identifier. * @return array{status: string, message: string} Operation result. */ public static function rollback( string $module ): array { $current = TMDO_Feature_Flags::get( $module ); if ( 'idle' === $current ) { return array( 'status' => 'idle', 'message' => 'Module is already idle.', ); } TMDO_Feature_Flags::reset( $module ); return array( 'status' => 'idle', 'message' => "Module rolled back from '{$current}' to idle.", ); } /** * Cleanup: stop writing to native postmeta. * * @param string $module Module identifier. * @return array{status: string, message: string} Operation result. */ public static function cleanup( string $module ): array { $current = TMDO_Feature_Flags::get( $module ); if ( 'cutover' !== $current ) { return array( 'status' => 'error', 'message' => "Module must be in 'cutover' state. Current: {$current}", ); } self::transition( $module, 'cleanup' ); return array( 'status' => 'cleanup', 'message' => 'Cleanup started. Native postmeta writes are stopped. Run enable to complete.', ); } /** * Enable: mark migration fully complete. * * @param string $module Module identifier. * @return array{status: string, message: string} Operation result. */ public static function enable( string $module ): array { $current = TMDO_Feature_Flags::get( $module ); if ( 'cleanup' !== $current ) { return array( 'status' => 'error', 'message' => "Module must be in 'cleanup' state. Current: {$current}", ); } self::transition( $module, 'complete' ); return array( 'status' => 'complete', 'message' => 'Module fully enabled. All reads and writes use the custom table.', ); } /** * Get comprehensive status for a module. * * @param string $module Module identifier. * @return array{module: string, state: string, zone: string, record: ?array} Module status. */ public static function status( string $module ): array { $migration = self::get_migration( $module ); $record = $migration ? $migration->get_record() : null; return array( 'module' => $module, 'state' => TMDO_Feature_Flags::get( $module ), 'zone' => $migration ? $migration->get_zone() : '', 'record' => $record, ); } }