, keep?: bool, version?: string}|null */ private $state = null; /** * Whether this request migrated older storage. * * @var bool */ private $migrated = false; /** * Option name for a module's settings. * * @since 1.0.0 * @param string $module_id Module ID. * @return string */ public static function option_name( $module_id ) { return self::PREFIX . $module_id; } /** * State read straight from the database before a write, so a change made by another request * since this one started (e.g. two switches flipped quickly) isn't overwritten. * * @return array{db: int, enabled: array, keep?: bool, version?: string} */ private function fresh_state() { global $wpdb; $raw = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", self::STATE ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $db = null === $raw ? null : maybe_unserialize( $raw ); if ( is_array( $db ) && (int) ( $db['db'] ?? 0 ) >= self::SCHEMA ) { $db['enabled'] = array_filter( (array) ( $db['enabled'] ?? array() ) ); $this->state = $db; } return $this->state(); } /** * State, migrating older storage on first read. * * @since 1.0.0 * @return array{db: int, enabled: array, keep?: bool, version?: string} */ private function state() { if ( null === $this->state ) { $state = get_option( self::STATE ); if ( ! is_array( $state ) || (int) ( $state['db'] ?? 0 ) < self::SCHEMA ) { $state = $this->migrate(); } $state['enabled'] = array_filter( (array) ( $state['enabled'] ?? array() ) ); $this->state = $state; } return $this->state; } /** * Bring stored data up to the current schema, one step at a time. * * 1 → 2: single `apwc_settings` blob → `apwc_state` + one option per module. * 2 → 3: "order-total-search" became "advanced-order-search" (tolerance carried over). * * @since 1.0.0 * @return array{db: int, enabled: array, keep?: bool, version?: string} */ private function migrate() { $state = get_option( self::STATE ); if ( ! is_array( $state ) ) { $state = array( 'db' => 1, 'enabled' => array(), ); } $state['enabled'] = (array) ( $state['enabled'] ?? array() ); if ( (int) $state['db'] < 2 ) { $legacy = get_option( self::LEGACY ); if ( is_array( $legacy ) && ! empty( $legacy['modules'] ) ) { foreach ( (array) $legacy['modules'] as $id => $module ) { $id = sanitize_key( $id ); if ( ! empty( $module['enabled'] ) ) { $state['enabled'][ $id ] = true; } if ( ! empty( $module['settings'] ) && is_array( $module['settings'] ) ) { // Autoload is corrected by Registry::sync_autoload() once modules are known. update_option( self::option_name( $id ), $module['settings'], false ); } } } delete_option( self::LEGACY ); $state['db'] = 2; } if ( (int) $state['db'] < 3 ) { if ( ! empty( $state['enabled']['order-total-search'] ) ) { $state['enabled']['advanced-order-search'] = true; } unset( $state['enabled']['order-total-search'] ); $old = get_option( self::option_name( 'order-total-search' ) ); if ( is_array( $old ) && isset( $old['tolerance'] ) && false === get_option( self::option_name( 'advanced-order-search' ) ) ) { update_option( self::option_name( 'advanced-order-search' ), array( 'tolerance' => $old['tolerance'] ), false ); } delete_option( self::option_name( 'order-total-search' ) ); $state['db'] = 3; } update_option( self::STATE, $state, true ); $this->migrated = true; return $state; } /** * Whether a module is switched on. * * @since 1.0.0 * @param string $module_id Module ID. * @return bool */ public function is_enabled( $module_id ) { return ! empty( $this->state()['enabled'][ $module_id ] ); } /** * Whether a module is switched on right now, read from the database rather than this * request's copy (for background jobs that may outlive a switch-off made by another request). * Also refreshes this request's copy. * * @since 1.0.0 * @param string $module_id Module ID. * @return bool */ public function is_enabled_fresh( $module_id ) { return ! empty( $this->fresh_state()['enabled'][ $module_id ] ); } /** * IDs of switched-on modules. * * @since 1.0.0 * @return string[] */ public function enabled_ids() { return array_keys( $this->state()['enabled'] ); } /** * Whether the merchant chose to keep this site's data when the plugin is deleted. * * @since 1.0.0 * @return bool */ public function keep_data() { return ! empty( $this->state()['keep'] ); } /** * Plugin version whose activation steps last ran on this site ('' before the first run). * * @since 1.0.0 * @return string */ public function version() { return (string) ( $this->state()['version'] ?? '' ); } /** * Remember that activation steps ran for this plugin version. * * @since 1.0.0 * @param string $version Plugin version. * @return void */ public function set_version( $version ) { $state = $this->fresh_state(); $state['version'] = (string) $version; $this->state = $state; update_option( self::STATE, $state, true ); } /** * Forget which version's activation steps ran, so they run again (Registry::maybe_upgrade()) * once the plugin can boot. Writes nothing when there is no stored state, and keeps the * option's autoload flag. * * @since 1.0.0 * @return void */ public function forget_version() { if ( ! is_array( get_option( self::STATE ) ) ) { return; } $state = $this->fresh_state(); if ( ! isset( $state['version'] ) ) { return; } unset( $state['version'] ); $this->state = $state; update_option( self::STATE, $state ); } /** * Keep (or remove) this site's data when the plugin is deleted. * * @since 1.0.0 * @param bool $keep Keep. * @return void */ public function set_keep_data( $keep ) { $state = $this->fresh_state(); if ( $keep ) { $state['keep'] = true; } else { unset( $state['keep'] ); } $this->state = $state; update_option( self::STATE, $state, true ); } /** * Switch a module on or off. * * @since 1.0.0 * @param string $module_id Module ID. * @param bool $enabled New state. * @return void */ public function set_enabled( $module_id, $enabled ) { $state = $this->fresh_state(); if ( $enabled ) { $state['enabled'][ $module_id ] = true; } else { unset( $state['enabled'][ $module_id ] ); } $this->state = $state; update_option( self::STATE, $state, true ); } /** * A module's stored settings. * * @since 1.0.0 * @param string $module_id Module ID. * @return array */ public function get_module_settings( $module_id ) { $raw = get_option( self::option_name( $module_id ), array() ); return is_array( $raw ) ? $raw : array(); } /** * Replace a module's settings. Caller must sanitize first. * * @since 1.0.0 * @param string $module_id Module ID. * @param array $settings Sanitized settings. * @param bool $autoload Whether the option should autoload. * @return void */ public function set_module_settings( $module_id, array $settings, $autoload ) { update_option( self::option_name( $module_id ), $settings, (bool) $autoload ); } /** * Make sure an autoloaded module's option exists. A module on its default settings has never * been saved, and a missing option costs a query on every request; an empty array (defaults * fill in) is loaded with the other autoloaded options for free. * * @since 1.0.0 * @param string $module_id Module ID. * @return void */ public function ensure_module_option( $module_id ) { add_option( self::option_name( $module_id ), array(), '', true ); // No-op when it exists. } /** * Whether storage was migrated during this request. * * @since 1.0.0 * @return bool */ public function was_migrated() { $this->state(); return $this->migrated; } /** * Forget the in-memory state (tests, and after external changes). * * @since 1.0.0 * @return void */ public function flush() { $this->state = null; } }