414 lines
9.5 KiB
PHP
414 lines
9.5 KiB
PHP
<?php
|
|
/**
|
|
* Database Migration Runner
|
|
*
|
|
* Orchestrates execution of database migrations, tracks migration history,
|
|
* and handles migration errors.
|
|
*
|
|
* @since 9.11.0
|
|
* @package MonsterInsights
|
|
*/
|
|
|
|
// Exit if accessed directly
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Migration Runner class.
|
|
*
|
|
* @since 9.11.0
|
|
*/
|
|
class MonsterInsights_Migration_Runner {
|
|
|
|
/**
|
|
* Registered migrations.
|
|
*
|
|
* @since 9.11.0
|
|
* @var array
|
|
*/
|
|
private $migrations = array();
|
|
|
|
/**
|
|
* Migration history option name.
|
|
*
|
|
* @since 9.11.0
|
|
* @var string
|
|
*/
|
|
const HISTORY_OPTION = 'monsterinsights_migration_history';
|
|
|
|
/**
|
|
* Migration lock option name.
|
|
*
|
|
* @since 9.11.0
|
|
* @var string
|
|
*/
|
|
const LOCK_OPTION = 'monsterinsights_migration_lock';
|
|
|
|
/**
|
|
* Lock timeout in seconds.
|
|
*
|
|
* @since 9.11.0
|
|
* @var int
|
|
*/
|
|
const LOCK_TIMEOUT = 300; // 5 minutes
|
|
|
|
/**
|
|
* Register a migration.
|
|
*
|
|
* @since 9.11.0
|
|
* @param string $migration_class Migration class name.
|
|
* @return bool True on success, false if class doesn't exist.
|
|
*/
|
|
public function register_migration( $migration_class ) {
|
|
if ( ! class_exists( $migration_class ) ) {
|
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
|
error_log( "MonsterInsights Migration Runner: Class {$migration_class} does not exist" );
|
|
return false;
|
|
}
|
|
|
|
// Instantiate migration
|
|
$migration = new $migration_class();
|
|
|
|
// Verify it extends MonsterInsights_Migration
|
|
if ( ! $migration instanceof MonsterInsights_Migration ) {
|
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
|
error_log( "MonsterInsights Migration Runner: {$migration_class} must extend MonsterInsights_Migration" );
|
|
return false;
|
|
}
|
|
|
|
// Add to migrations array
|
|
$version = $migration->get_version();
|
|
|
|
// Check for duplicate version
|
|
if ( isset( $this->migrations[ $version ] ) ) {
|
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
|
error_log( "MonsterInsights Migration Runner: Duplicate version {$version} - {$migration_class} conflicts with existing migration" );
|
|
return false;
|
|
}
|
|
|
|
$this->migrations[ $version ] = $migration;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Register multiple migrations.
|
|
*
|
|
* @since 9.11.0
|
|
* @param array $migration_classes Array of migration class names.
|
|
* @return int Number of migrations successfully registered.
|
|
*/
|
|
public function register_migrations( $migration_classes ) {
|
|
$count = 0;
|
|
foreach ( $migration_classes as $migration_class ) {
|
|
if ( $this->register_migration( $migration_class ) ) {
|
|
$count++;
|
|
}
|
|
}
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Get all registered migrations.
|
|
*
|
|
* @since 9.11.0
|
|
* @return array Array of migration objects.
|
|
*/
|
|
public function get_migrations() {
|
|
return $this->migrations;
|
|
}
|
|
|
|
/**
|
|
* Get pending migrations that haven't been run yet.
|
|
*
|
|
* @since 9.11.0
|
|
* @return array Array of migration objects.
|
|
*/
|
|
public function get_pending_migrations() {
|
|
$history = $this->get_migration_history();
|
|
$completed = wp_list_pluck( $history, 'version' );
|
|
|
|
$pending = array();
|
|
foreach ( $this->migrations as $version => $migration ) {
|
|
if ( ! in_array( $version, $completed, true ) ) {
|
|
$pending[ $version ] = $migration;
|
|
}
|
|
}
|
|
|
|
// Sort by version
|
|
uksort( $pending, 'version_compare' );
|
|
|
|
return $pending;
|
|
}
|
|
|
|
/**
|
|
* Check if there are pending migrations.
|
|
*
|
|
* @since 9.11.0
|
|
* @return bool True if migrations are pending, false otherwise.
|
|
*/
|
|
public function has_pending_migrations() {
|
|
return ! empty( $this->get_pending_migrations() );
|
|
}
|
|
|
|
/**
|
|
* Run all pending migrations.
|
|
*
|
|
* @since 9.11.0
|
|
* @return array {
|
|
* Migration results.
|
|
*
|
|
* @type bool $success Whether all migrations completed successfully.
|
|
* @type array $results Array of individual migration results.
|
|
* }
|
|
*/
|
|
public function run_pending_migrations() {
|
|
// Check for lock
|
|
if ( ! $this->acquire_lock() ) {
|
|
return array(
|
|
'success' => false,
|
|
'error' => 'Migration is already running',
|
|
'results' => array(),
|
|
);
|
|
}
|
|
|
|
$pending = $this->get_pending_migrations();
|
|
$results = array();
|
|
$success = true;
|
|
|
|
foreach ( $pending as $version => $migration ) {
|
|
$result = $this->run_migration( $migration );
|
|
$results[ $version ] = $result;
|
|
|
|
if ( ! $result['success'] ) {
|
|
$success = false;
|
|
// Stop on first failure
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Release lock
|
|
$this->release_lock();
|
|
|
|
return array(
|
|
'success' => $success,
|
|
'results' => $results,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Run a single migration.
|
|
*
|
|
* @since 9.11.0
|
|
* @param MonsterInsights_Migration $migration Migration instance.
|
|
* @return array {
|
|
* Migration result.
|
|
*
|
|
* @type bool $success Whether migration completed successfully.
|
|
* @type string $error Error message if failed.
|
|
* @type float $duration Time taken in seconds.
|
|
* }
|
|
*/
|
|
public function run_migration( $migration ) {
|
|
$version = $migration->get_version();
|
|
$description = $migration->get_description();
|
|
$start_time = microtime( true );
|
|
|
|
// Log start
|
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
|
error_log( "MonsterInsights: Running migration {$version}: {$description}" );
|
|
|
|
try {
|
|
// Run the migration
|
|
$migration->up();
|
|
|
|
// Calculate duration
|
|
$duration = microtime( true ) - $start_time;
|
|
|
|
// Record in history
|
|
$this->record_migration( $version, 'completed', $duration );
|
|
|
|
// Log success
|
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
|
error_log( sprintf(
|
|
'MonsterInsights: Migration %s completed successfully in %.2f seconds',
|
|
$version,
|
|
$duration
|
|
) );
|
|
|
|
return array(
|
|
'success' => true,
|
|
'duration' => $duration,
|
|
);
|
|
|
|
} catch ( Exception $e ) {
|
|
// Calculate duration
|
|
$duration = microtime( true ) - $start_time;
|
|
|
|
// Record failure in history
|
|
$this->record_migration( $version, 'failed', $duration, $e->getMessage() );
|
|
|
|
// Log error
|
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
|
error_log( sprintf(
|
|
'MonsterInsights: Migration %s failed after %.2f seconds: %s',
|
|
$version,
|
|
$duration,
|
|
$e->getMessage()
|
|
) );
|
|
|
|
return array(
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
'duration' => $duration,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get migration history.
|
|
*
|
|
* @since 9.11.0
|
|
* @return array Array of migration history entries.
|
|
*/
|
|
public function get_migration_history() {
|
|
return get_option( self::HISTORY_OPTION, array() );
|
|
}
|
|
|
|
/**
|
|
* Record a migration in the history.
|
|
*
|
|
* @since 9.11.0
|
|
* @param string $version Migration version.
|
|
* @param string $status Status (completed, failed).
|
|
* @param float $duration Time taken in seconds.
|
|
* @param string $error Error message if failed.
|
|
*/
|
|
private function record_migration( $version, $status, $duration, $error = '' ) {
|
|
$history = $this->get_migration_history();
|
|
|
|
$history[] = array(
|
|
'version' => $version,
|
|
'status' => $status,
|
|
'duration' => $duration,
|
|
'error' => $error,
|
|
'timestamp' => current_time( 'mysql' ),
|
|
);
|
|
|
|
update_option( self::HISTORY_OPTION, $history, false );
|
|
}
|
|
|
|
/**
|
|
* Clear migration history.
|
|
*
|
|
* Useful for testing or troubleshooting.
|
|
*
|
|
* @since 9.11.0
|
|
* @return bool True on success, false on failure.
|
|
*/
|
|
public function clear_history() {
|
|
return delete_option( self::HISTORY_OPTION );
|
|
}
|
|
|
|
/**
|
|
* Acquire migration lock to prevent concurrent runs.
|
|
*
|
|
* @since 9.11.0
|
|
* @return bool True if lock acquired, false if already locked.
|
|
*/
|
|
private function acquire_lock() {
|
|
$lock = get_option( self::LOCK_OPTION );
|
|
|
|
// Check if lock exists and is still valid
|
|
if ( $lock && ( time() - $lock ) < self::LOCK_TIMEOUT ) {
|
|
return false;
|
|
}
|
|
|
|
// Set new lock
|
|
update_option( self::LOCK_OPTION, time(), false );
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Release migration lock.
|
|
*
|
|
* @since 9.11.0
|
|
* @return bool True on success, false on failure.
|
|
*/
|
|
private function release_lock() {
|
|
return delete_option( self::LOCK_OPTION );
|
|
}
|
|
|
|
/**
|
|
* Force release migration lock.
|
|
*
|
|
* Useful if migration process dies and lock isn't released.
|
|
*
|
|
* @since 9.11.0
|
|
* @return bool True on success, false on failure.
|
|
*/
|
|
public function force_release_lock() {
|
|
return $this->release_lock();
|
|
}
|
|
|
|
/**
|
|
* Check if migrations are currently locked.
|
|
*
|
|
* @since 9.11.0
|
|
* @return bool True if locked, false otherwise.
|
|
*/
|
|
public function is_locked() {
|
|
$lock = get_option( self::LOCK_OPTION );
|
|
|
|
if ( ! $lock ) {
|
|
return false;
|
|
}
|
|
|
|
// Check if lock has expired
|
|
if ( ( time() - $lock ) >= self::LOCK_TIMEOUT ) {
|
|
// Lock expired, release it
|
|
$this->release_lock();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get migration statistics.
|
|
*
|
|
* @since 9.11.0
|
|
* @return array {
|
|
* Migration statistics.
|
|
*
|
|
* @type int $total_migrations Total registered migrations.
|
|
* @type int $pending_migrations Number of pending migrations.
|
|
* @type int $completed_migrations Number of completed migrations.
|
|
* @type int $failed_migrations Number of failed migrations.
|
|
* }
|
|
*/
|
|
public function get_statistics() {
|
|
$history = $this->get_migration_history();
|
|
$pending = $this->get_pending_migrations();
|
|
|
|
$completed = 0;
|
|
$failed = 0;
|
|
|
|
foreach ( $history as $entry ) {
|
|
if ( $entry['status'] === 'completed' ) {
|
|
$completed++;
|
|
} elseif ( $entry['status'] === 'failed' ) {
|
|
$failed++;
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'total_migrations' => count( $this->migrations ),
|
|
'pending_migrations' => count( $pending ),
|
|
'completed_migrations' => $completed,
|
|
'failed_migrations' => $failed,
|
|
);
|
|
}
|
|
}
|