Source: includes/internal/installer/class-installer.php

<?php
/**
 * File containing the class \Sensei\Internal\Installer\Installer.
 *
 * @package sensei
 * @since 4.16.1
 */

namespace Sensei\Internal\Installer;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Installer class.
 *
 * Responsible for running DB updates that need to be run per version.
 *
 * @internal
 *
 * @since 4.16.1
 */
class Installer {

	private const SENSEI_VERSION_OPTION_NAME = 'sensei-version';
	const SENSEI_INSTALL_VERSION_OPTION_NAME = 'sensei-install-version';

	/**
	 * Instance of the class.
	 *
	 * @since 4.16.1
	 * @var self
	 */
	private static $instance;

	/**
	 * The database schema class.
	 *
	 * @since 4.16.1
	 * @var Schema
	 */
	private $schema;

	/**
	 * Sensei Updates factory.
	 *
	 * @var Updates_Factory
	 */
	private $updates_factory;
	/**
	 * Current Sensei version.
	 *
	 * @var string|null
	 */
	private $version;

	/**
	 * Constructor.
	 *
	 * @internal
	 *
	 * @since 4.16.1
	 *
	 * @param Schema          $schema Schema migration object.
	 * @param Updates_Factory $updates_factory Updates factory object.
	 * @param string|null     $version Current Sensei version.
	 */
	public function __construct( Schema $schema, Updates_Factory $updates_factory, ?string $version ) {
		$this->schema          = $schema;
		$this->updates_factory = $updates_factory;
		$this->version         = $version;
	}

	/**
	 * Fetches an instance of the class.
	 *
	 * @internal
	 *
	 * @since 4.16.1
	 *
	 * @param string|null $version Current Sensei version.
	 * @return self
	 */
	public static function instance( ?string $version = null ): self {
		if ( ! self::$instance ) {
			$schema         = new Schema( Sensei()->feature_flags );
			self::$instance = new self( $schema, new Updates_Factory(), $version );
		}

		return self::$instance;
	}

	/**
	 * Initialize necessary hooks.
	 *
	 * @internal
	 *
	 * @since 4.16.1
	 */
	public function init() {
		register_activation_hook( SENSEI_LMS_PLUGIN_FILE, [ $this, 'install' ] );
		add_action( 'plugins_loaded', [ $this, 'install' ] );
		add_action( 'init', array( $this, 'update' ) );
	}

	/**
	 * Run the installer.
	 *
	 * This method is executed when the plugin is installed or updated.
	 *
	 * @internal
	 *
	 * @since 4.16.1
	 */
	public function install() {
		if (
			! is_blog_installed()
			|| $this->is_installing()
			|| ! $this->requires_install()
		) {
			return;
		}

		set_transient( 'sensei_lms_installing', 'yes', MINUTE_IN_SECONDS * 10 );

		$this->schema->create_tables();

		delete_transient( 'sensei_lms_installing' );

		/**
		 * Fires after the installation completes.
		 *
		 * @since 4.16.1
		 *
		 * @hook sensei_lms_installed
		 */
		do_action( 'sensei_lms_installed' );
	}

	/**
	 * Checks for plugin update tasks and ensures the current version is set.
	 *
	 * @internal
	 *
	 * @since 4.16.1
	 */
	public function update(): void {
		$current_version = get_option( self::SENSEI_VERSION_OPTION_NAME );
		$is_new_install  = $this->is_new_install( $current_version );
		$updates         = $this->updates_factory->create( $current_version, $this->version, $is_new_install );

		$updates->run_updates();
		$this->update_version( $is_new_install );
	}

	/**
	 * Get the Schema instance.
	 *
	 * @internal
	 *
	 * @since 4.16.1
	 *
	 * @return Schema
	 */
	public function get_schema(): Schema {
		return $this->schema;
	}

	/**
	 * Check if the installer is running.
	 *
	 * @since 4.16.1
	 *
	 * @return bool
	 */
	private function is_installing(): bool {
		return 'yes' === get_transient( 'sensei_lms_installing' );
	}

	/**
	 * Determine if the installer needs to be run by checking the plugin's version.
	 *
	 * @since 4.16.1
	 *
	 * @return bool
	 */
	private function requires_install(): bool {
		$version = get_option( self::SENSEI_VERSION_OPTION_NAME );

		return version_compare( $version, SENSEI_LMS_VERSION, '<' );
	}

	/**
	 * Update the plugin's version to the current one.
	 *
	 * @since 4.16.1
	 *
	 * @param bool $is_new_install Whether this is a new install.
	 */
	private function update_version( bool $is_new_install ): void {
		if ( ! $this->version ) {
			return;
		}
		update_option( self::SENSEI_VERSION_OPTION_NAME, $this->version );

		if ( $is_new_install ) {
			update_option( self::SENSEI_INSTALL_VERSION_OPTION_NAME, $this->version );
		}
	}

	/**
	 * Checks if the plugin is being installed for the first time.
	 *
	 * @param mixed $current_version The current version of the plugin, based on settings.
	 *
	 * @return bool
	 */
	private function is_new_install( $current_version ): bool {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Lightweight query run only once before post type is registered.
		$course_sample_id = (int) $wpdb->get_var( "SELECT `ID` FROM {$wpdb->posts} WHERE `post_type`='course' LIMIT 1" );

		return ! $current_version && empty( $course_sample_id );
	}
}