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

304 lines
9.6 KiB
PHP

<?php
/**
* TMDO_V2_Upgrader — atomic v1.3.x → v2.0.0 upgrade orchestrator (PR-7).
*
* Implements the Part F upgrade strategy from the master plan:
* 1. Pre-flight: PHP / WP / MySQL versions, free disk, no in-flight migrations
* 2. Backup wpdo_features option (rollback safety net)
* 3. dbDelta v2 tables (idempotent)
* 4. ALTER existing tables (skip if already migrated)
* 5. UAE import (when wp_uae_* present, otherwise no-op)
* 6. migrate_feature_flags_v2 — seed entity module states
* 7. update wpdo_db_version → 2.0.0
* 8. safely_deactivate_uae_plugin (when present)
* 9. Schedule cron to remove the UAE plugin directory (5s)
* 10. fire wpdo_v2_upgraded action
*
* Any throw → catch → write wpdo_v2_upgrade_error → restore features → admin
* notice. New tables are NOT dropped on failure (idempotent retry).
*
* @package WP_Data_Optimizer
* @since 2.0.0
*/
declare(strict_types=1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Atomic upgrade orchestrator.
*/
final class TMDO_V2_Upgrader {
/** Target schema version after successful upgrade. */
public const TARGET_VERSION = '2.0.0';
/** Min PHP / WP / MySQL versions enforced by pre-flight. */
public const MIN_PHP_VERSION = '8.1';
public const MIN_WP_VERSION = '6.0';
public const MIN_MYSQL_VERSION = '5.7';
/** Free-disk threshold in MB. */
public const MIN_FREE_DISK_MB = 100;
/**
* Pre-flight check — returns map of check_name => bool.
*
* @param array $opts Optional overrides (mostly for tests).
* @return array<string,bool>
*/
public static function pre_flight_check( array $opts = array() ): array {
global $wpdb;
$wp_version = $opts['wp_version'] ?? ( defined( 'ABSPATH' ) ? ( get_bloginfo( 'version' ) ?: '6.0' ) : '6.0' );
$mysql_version = $opts['mysql_version'] ?? self::detect_mysql_version();
$disk_path = $opts['disk_path'] ?? sys_get_temp_dir();
$checks = array(
'php_version' => version_compare( PHP_VERSION, self::MIN_PHP_VERSION, '>=' ),
'wp_version' => version_compare( $wp_version, self::MIN_WP_VERSION, '>=' ),
'mysql_version' => version_compare( $mysql_version, self::MIN_MYSQL_VERSION, '>=' ),
'free_disk_mb' => self::detect_free_disk_mb( $disk_path ) >= self::MIN_FREE_DISK_MB,
'features_writable' => self::is_features_option_writable(),
'no_active_migration' => self::no_active_migration(),
);
return $checks;
}
/**
* Run the atomic upgrade. Returns true on success, false on failure.
*
* @return bool
* @throws \RuntimeException When UAE importer fails (caught internally and rolled back).
*/
public static function upgrade_to_v2(): bool {
// 1. Pre-flight check.
$checks = self::pre_flight_check();
if ( in_array( false, $checks, true ) ) {
update_option(
'wpdo_v2_upgrade_error',
'Pre-flight check failed: ' . wp_json_encode( $checks ),
false
);
update_option( 'wpdo_v2_upgrade_status', 'preflight_failed', false );
return false;
}
update_option( 'wpdo_v2_upgrade_started_at', time(), false );
update_option( 'wpdo_v2_upgrade_status', 'in_progress', false );
$features_backup = get_option( 'wpdo_features', array() );
update_option( 'wpdo_v2_features_backup', $features_backup, false );
try {
// 4. Install v2 tables.
TMDO_Installer::install_v2_tables();
// 5. UAE import (when applicable).
if ( self::detect_uae_data() ) {
$import_ok = self::run_uae_import();
if ( ! $import_ok ) {
throw new \RuntimeException( 'UAE importer failed' );
}
}
// 6. Migrate feature flags to include entity modules.
self::migrate_feature_flags_v2();
// 7. Bump db_version.
update_option( 'wpdo_db_version', self::TARGET_VERSION, false );
update_option( 'wpdo_v2_upgrade_status', 'complete', false );
update_option( 'wpdo_v2_upgraded_at', time(), false );
// 8. Safely deactivate UAE plugin.
self::safely_deactivate_uae_plugin();
// 9. Schedule plugin directory removal.
self::schedule_uae_dir_removal();
// 10. Fire hook for downstream consumers.
do_action( 'wpdo_v2_upgraded', '1.3.38', self::TARGET_VERSION );
return true;
} catch ( \Throwable $e ) {
// Rollback path: restore features option, leave new tables in place
// (idempotent retry on next attempt).
update_option( 'wpdo_features', $features_backup );
update_option( 'wpdo_v2_upgrade_error', $e->getMessage(), false );
update_option( 'wpdo_v2_upgrade_status', 'failed', false );
return false;
}
}
/**
* Roll back from a partially-completed v2 upgrade.
*
* @param bool $keep_data When true, retain wpdo_uni_* tables (for retry).
* @return bool
*/
public static function rollback_v2( bool $keep_data = true ): bool {
$backup = get_option( 'wpdo_v2_features_backup' );
if ( false !== $backup ) {
update_option( 'wpdo_features', $backup );
}
update_option( 'wpdo_db_version', '1.0.0', false );
update_option( 'wpdo_v2_upgrade_status', 'rolled_back', false );
if ( ! $keep_data ) {
global $wpdb;
foreach ( array( 'wpdo_audit', 'wpdo_shadow_diffs', 'wpdo_site_metrics', 'wpdo_uni_options' ) as $t ) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $t from hardcoded array, $wpdb->prefix sanitized by core.
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}{$t}`" );
}
}
return true;
}
// ── Helpers ─────────────────────────────────────────────────────────────
/**
* Detect whether wp_uae_* tables exist (S3 scenario).
*/
public static function detect_uae_data(): bool {
global $wpdb;
$count = (int) $wpdb->get_var(
$wpdb->prepare(
'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE %s',
$wpdb->prefix . 'uae_%'
)
);
return $count > 0;
}
/**
* Run the UAE → WPDO data importer. Stub for v2.0.0 — full implementation
* lives in includes/migration/class-tmdo-uae-importer.php (PR-7 follow-up).
*
* @return bool
*/
public static function run_uae_import(): bool {
if ( class_exists( 'TMDO_UAE_Importer' ) ) {
return TMDO_UAE_Importer::run(
array(
'auto' => true,
'keep_source' => true,
)
);
}
// No UAE importer yet — return true so dev10 (no UAE data) progresses.
return true;
}
/**
* Migrate the feature_flags option to include entity modules.
*/
public static function migrate_feature_flags_v2(): void {
$flags = get_option( 'wpdo_features', array() );
if ( ! is_array( $flags ) ) {
$flags = array();
}
foreach ( array( 'entity_user', 'entity_term', 'entity_comment', 'entity_options' ) as $module ) {
if ( ! isset( $flags[ $module ] ) ) {
$flags[ $module ] = 'idle';
}
}
update_option( 'wpdo_features', $flags );
}
/**
* Detect installed MySQL / MariaDB version via @@version.
*/
public static function detect_mysql_version(): string {
global $wpdb;
try {
$v = (string) $wpdb->get_var( 'SELECT VERSION()' );
// Strip trailing -MariaDB or similar tags for version_compare.
if ( preg_match( '/^([\d.]+)/', $v, $m ) ) {
return $m[1];
}
} catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- intentional: fall through to safe default.
// Fall through to safe default.
}
return '5.7';
}
/**
* Detect free disk space in MB at the given path.
*
* @param string $path Filesystem path (typically sys_get_temp_dir()).
* @return int Free disk space in MB, or 0 when unreadable.
*/
public static function detect_free_disk_mb( string $path ): int {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- @ suppresses E_WARNING when path is on a stat-fail filesystem; explicit false-check follows.
$free = @disk_free_space( $path );
if ( false === $free ) {
return 0;
}
return (int) ( $free / 1024 / 1024 );
}
/**
* Verify wpdo_features option is writable (autoload=no row exists or absent).
*/
private static function is_features_option_writable(): bool {
$probe_key = '_wpdo_v2_writability_probe_' . wp_generate_password( 12, false );
$ok = update_option( $probe_key, time(), false );
delete_option( $probe_key );
return $ok;
}
/**
* Verify no migration is currently in-flight (zero rows in 'in_progress' state).
*/
private static function no_active_migration(): bool {
global $wpdb;
try {
$count = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM `{$wpdb->prefix}wpdo_migrations` WHERE state = %s",
'in_progress'
)
);
return 0 === $count;
} catch ( \Throwable $e ) {
// Table may not exist on truly fresh installs — treat as "no active migration".
return true;
}
}
/**
* Quietly deactivate wp-universal-anti-eav plugin if it is currently active.
*/
private static function safely_deactivate_uae_plugin(): void {
if ( ! function_exists( 'deactivate_plugins' ) || ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( ! function_exists( 'is_plugin_active' ) ) {
return;
}
$slug = 'wp-universal-anti-eav/wp-universal-anti-eav.php';
if ( is_plugin_active( $slug ) ) {
deactivate_plugins( $slug, true );
}
}
/**
* Schedule a one-shot cron event to remove the UAE plugin directory.
*
* Filesystem permission failures fall back to admin notice for manual removal.
*/
private static function schedule_uae_dir_removal(): void {
if ( ! function_exists( 'wp_schedule_single_event' ) ) {
return;
}
if ( ! wp_next_scheduled( 'wpdo_remove_uae_plugin_dir' ) ) {
wp_schedule_single_event( time() + 5, 'wpdo_remove_uae_plugin_dir' );
}
}
}