version => migrationInstance */ private array $version_migrations; private Logger $logger; private Upgrade_Notice $upgrade_notice; private Cache_Flusher $cache_flusher; public function __construct( Plugin_Meta $plugin_meta, Settings_Storage $settings, Logger $logger, Upgrade_Notice $upgrade_notice, Cache_Flusher $cache_flusher ) { $this->plugin_meta = $plugin_meta; $this->settings = $settings; $this->logger = $logger; $this->upgrade_notice = $upgrade_notice; $this->cache_flusher = $cache_flusher; $this->version_migrations = array(); } public static function has_route_hooks( Route_Detector $route_detector ): bool { // avoid requests with incomplete hooks cycle. return $route_detector->is_complete_cycle_request(); } public function set_route_hooks( Route_Detector $route_detector ): void { // don't use 'upgrader_process_complete' hook, as user can update the plugin manually by FTP. $db_version = $this->settings->get_version(); $code_version = $this->plugin_meta->get_version(); /** * Run upgrade if the DB version is set, and different from the code version. * (it's unset until the plugin activation hook called, which happens later than wp_loaded) */ if ( '' === $db_version || $db_version === $code_version ) { return; } // only at this hook can be sure that other plugin's functions are available. self::add_action( 'plugins_loaded', array( $this, 'migrate_from_previous_version', ), // with the priority higher than in the Data_Vendors. Field_Provider_Cluster::PLUGINS_LOADED_HOOK_PRIORITY + 1 ); /** * Running lately, inside the "wp_loaded" hook ensures all migration hooks are called. * * Plus, we ensure that migration wasn't interrupted by some redirect or another request-breaker: * otherwise, we don't save the new version and will have another migration. */ self::add_action( 'wp_loaded', array( $this, 'complete_version_migration' ) ); } public static function is_version_lower( string $version, string $target_version ): bool { if ( ! self::is_valid_version( $version ) || ! self::is_valid_version( $target_version ) ) { return false; } $current_version = explode( '.', $version ); $target_version = explode( '.', $target_version ); // check for IDE. if ( 3 !== count( $current_version ) || 3 !== count( $target_version ) ) { return false; } // convert to int. foreach ( $current_version as &$part ) { $part = int( $part ); } foreach ( $target_version as &$part ) { $part = int( $part ); } // compare. // major. if ( $current_version[0] > $target_version[0] ) { return false; } elseif ( $current_version[0] < $target_version[0] ) { return true; } // minor. if ( $current_version[1] > $target_version[1] ) { return false; } elseif ( $current_version[1] < $target_version[1] ) { return true; } // patch. if ( $current_version[2] >= $target_version[2] ) { return false; } return true; } public static function is_valid_version( string $version ): bool { return 1 === preg_match( '/^\d+\.\d+\.\d+$/', $version ); } /** * @param Version_Migration[] $version_migrations */ public function add_version_migrations( array $version_migrations ): void { foreach ( $version_migrations as $version_migration ) { $version = $version_migration->introduced_version(); $this->version_migrations[ $version ] = $version_migration; } } public function migrate_cpt_settings( Cpt_Settings $cpt_settings ): void { $previous_version = $cpt_settings->plugin_version; // we don't need it for instances outside of Git repository. $cpt_settings->plugin_version = ''; $version_migrations = $this->get_version_migrations( $previous_version ); foreach ( $version_migrations as $version_migration ) { $version_migration->migrate_cpt_settings( $cpt_settings ); } } public function migrate_from_previous_version(): void { // previous error logs are not relevant anymore. $this->logger->clear_error_logs(); // all versions since 1.6.0 have a DB version record. $previous_version = $this->settings->get_version(); $this->migrate_from_version( $previous_version ); } public function complete_version_migration(): void { $this->cache_flusher->flush_caches(); $this->update_db_plugin_version(); $this->logger->info( 'Version upgrade is complete' ); } public function migrate_from_version( string $from_version ): void { $version_migrations = $this->get_version_migrations( $from_version ); $version_migration_names = array_map( fn( Version_Migration $version_migration )=> $this->get_migration_name( $version_migration ), $version_migrations ); $this->logger->info( 'Performing version upgrade', array( 'previous_version' => $from_version, 'current_version' => $this->plugin_meta->get_version(), 'migrations' => $version_migration_names, ) ); foreach ( $version_migrations as $version_migration ) { $this->logger->info( 'Running version migration case', array( 'migration' => $this->get_migration_name( $version_migration ), ) ); $version_migration->migrate(); } $this->upgrade_notice->setup_upgrade_notice( $version_migrations ); } protected function update_db_plugin_version(): void { $this->settings->set_version( $this->plugin_meta->get_version() ); $this->settings->save(); } /** * @return Version_Migration[] */ protected function get_version_migrations( string $from_version ): array { $target_migrations = array_filter( $this->version_migrations, fn( Version_Migration $version_migration ) => self::is_version_lower( $from_version, $version_migration->introduced_version() ) ); // ASC sort. usort( $target_migrations, fn( Version_Migration $first, Version_Migration $second ) => $first->get_order() <=> $second->get_order() ); return $target_migrations; } protected function get_migration_name( Migration $migration ): string { $full_class_name = get_class( $migration ); $class_name_parts = explode( '\\', $full_class_name ); return end( $class_name_parts ); } }