You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

477 lines
14 KiB

9 months ago
<?php
/**
* @package Freemius
* @copyright Copyright (c) 2015, Freemius, Inc.
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
* @since 1.0.3
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* 2-layer lazy options manager.
* layer 2: Memory
* layer 1: Database (options table). All options stored as one option record in the DB to reduce number of DB queries.
*
* If load() is not explicitly called, starts as empty manager. Same thing about saving the data - you have to explicitly call store().
*
* Class Freemius_Option_Manager
*/
class FS_Option_Manager {
/**
* @var string
*/
private $_id;
/**
* @var array|object
*/
private $_options;
/**
* @var FS_Logger
*/
private $_logger;
/**
* @since 2.0.0
* @var int The ID of the blog that is associated with the current site level options.
*/
private $_blog_id = 0;
/**
* @since 2.0.0
* @var bool
*/
private $_is_network_storage;
/**
* @var bool|null
*/
private $_autoload;
/**
* @var array[string]FS_Option_Manager {
* @key string
* @value FS_Option_Manager
* }
*/
private static $_MANAGERS = array();
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.3
*
* @param string $id
* @param bool $load
* @param bool|int $network_level_or_blog_id Since 2.0.0
* @param bool|null $autoload
*/
private function __construct(
$id,
$load = false,
$network_level_or_blog_id = false,
$autoload = null
) {
$id = strtolower( $id );
$this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_opt_mngr_' . $id, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
$this->_logger->entrance();
$this->_logger->log( 'id = ' . $id );
$this->_id = $id;
$this->_autoload = $autoload;
if ( is_multisite() ) {
$this->_is_network_storage = ( true === $network_level_or_blog_id );
if ( is_numeric( $network_level_or_blog_id ) ) {
$this->_blog_id = $network_level_or_blog_id;
}
} else {
$this->_is_network_storage = false;
}
if ( $load ) {
$this->load();
}
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.3
*
* @param string $id
* @param bool $load
* @param bool|int $network_level_or_blog_id Since 2.0.0
* @param bool|null $autoload
*
* @return \FS_Option_Manager
*/
static function get_manager(
$id,
$load = false,
$network_level_or_blog_id = false,
$autoload = null
) {
$key = strtolower( $id );
if ( is_multisite() ) {
if ( true === $network_level_or_blog_id ) {
$key .= ':ms';
} else if ( is_numeric( $network_level_or_blog_id ) && $network_level_or_blog_id > 0 ) {
$key .= ":{$network_level_or_blog_id}";
} else {
$network_level_or_blog_id = get_current_blog_id();
$key .= ":{$network_level_or_blog_id}";
}
}
if ( ! isset( self::$_MANAGERS[ $key ] ) ) {
self::$_MANAGERS[ $key ] = new FS_Option_Manager(
$id,
$load,
$network_level_or_blog_id,
$autoload
);
} // If load required but not yet loaded, load.
else if ( $load && ! self::$_MANAGERS[ $key ]->is_loaded() ) {
self::$_MANAGERS[ $key ]->load();
}
return self::$_MANAGERS[ $key ];
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.3
*
* @param bool $flush
*/
function load( $flush = false ) {
$this->_logger->entrance();
if ( ! $flush && isset( $this->_options ) ) {
return;
}
if ( isset( $this->_options ) ) {
// Clear prev options.
$this->clear();
}
$option_name = $this->get_option_manager_name();
if ( $this->_is_network_storage ) {
$this->_options = get_site_option( $option_name );
} else if ( $this->_blog_id > 0 ) {
$this->_options = get_blog_option( $this->_blog_id, $option_name );
} else {
$this->_options = get_option( $option_name );
}
if ( is_string( $this->_options ) ) {
$this->_options = json_decode( $this->_options );
}
// $this->_logger->info('get_option = ' . var_export($this->_options, true));
if ( false === $this->_options ) {
$this->clear();
}
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.3
*
* @return bool
*/
function is_loaded() {
return isset( $this->_options );
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.3
*
* @return bool
*/
function is_empty() {
return ( $this->is_loaded() && false === $this->_options );
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.6
*
* @param bool $flush
*/
function clear( $flush = false ) {
$this->_logger->entrance();
$this->_options = array();
if ( $flush ) {
$this->store();
}
}
/**
* Delete options manager from DB.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*/
function delete() {
$option_name = $this->get_option_manager_name();
if ( $this->_is_network_storage ) {
delete_site_option( $option_name );
} else if ( $this->_blog_id > 0 ) {
delete_blog_option( $this->_blog_id, $option_name );
} else {
delete_option( $option_name );
}
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.6
*
* @param string $option
* @param bool $flush
*
* @return bool
*/
function has_option( $option, $flush = false ) {
if ( ! $this->is_loaded() || $flush ) {
$this->load( $flush );
}
return array_key_exists( $option, $this->_options );
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.3
*
* @param string $option
* @param mixed $default
* @param bool $flush
*
* @return mixed
*/
function get_option( $option, $default = null, $flush = false ) {
$this->_logger->entrance( 'option = ' . $option );
if ( ! $this->is_loaded() || $flush ) {
$this->load( $flush );
}
if ( is_array( $this->_options ) ) {
$value = isset( $this->_options[ $option ] ) ?
$this->_options[ $option ] :
$default;
} else if ( is_object( $this->_options ) ) {
$value = isset( $this->_options->{$option} ) ?
$this->_options->{$option} :
$default;
} else {
$value = $default;
}
/**
* If it's an object, return a clone of the object, otherwise,
* external changes of the object will actually change the value
* of the object in the option manager which may lead to an unexpected
* behaviour and data integrity when a store() call is triggered.
*
* Example:
* $object1 = $options->get_option( 'object1' );
* $object1->x = 123;
*
* $object2 = $options->get_option( 'object2' );
* $object2->y = 'dummy';
*
* $options->set_option( 'object2', $object2, true );
*
* If we don't return a clone of option 'object1', setting 'object2'
* will also store the updated value of 'object1' which is quite not
* an expected behaviour.
*
* @author Vova Feldman
*/
return is_object( $value ) ? clone $value : $value;
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.3
*
* @param string $option
* @param mixed $value
* @param bool $flush
*/
function set_option( $option, $value, $flush = false ) {
$this->_logger->entrance( 'option = ' . $option );
if ( ! $this->is_loaded() ) {
$this->clear();
}
/**
* If it's an object, store a clone of the object, otherwise,
* external changes of the object will actually change the value
* of the object in the options manager which may lead to an unexpected
* behaviour and data integrity when a store() call is triggered.
*
* Example:
* $object1 = new stdClass();
* $object1->x = 123;
*
* $options->set_option( 'object1', $object1 );
*
* $object1->x = 456;
*
* $options->set_option( 'object2', $object2, true );
*
* If we don't set the option as a clone of option 'object1', setting 'object2'
* will also store the updated value of 'object1' ($object1->x = 456 instead of
* $object1->x = 123) which is quite not an expected behaviour.
*
* @author Vova Feldman
*/
$copy = is_object( $value ) ? clone $value : $value;
if ( is_array( $this->_options ) ) {
$this->_options[ $option ] = $copy;
} else if ( is_object( $this->_options ) ) {
$this->_options->{$option} = $copy;
}
if ( $flush ) {
$this->store();
}
}
/**
* Unset option.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.3
*
* @param string $option
* @param bool $flush
*/
function unset_option( $option, $flush = false ) {
$this->_logger->entrance( 'option = ' . $option );
if ( is_array( $this->_options ) ) {
if ( ! isset( $this->_options[ $option ] ) ) {
return;
}
unset( $this->_options[ $option ] );
} else if ( is_object( $this->_options ) ) {
if ( ! isset( $this->_options->{$option} ) ) {
return;
}
unset( $this->_options->{$option} );
}
if ( $flush ) {
$this->store();
}
}
/**
* Dump options to database.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.3
*/
function store() {
$this->_logger->entrance();
$option_name = $this->get_option_manager_name();
if ( $this->_logger->is_on() ) {
$this->_logger->info( $option_name . ' = ' . var_export( $this->_options, true ) );
}
// Update DB.
if ( $this->_is_network_storage ) {
update_site_option( $option_name, $this->_options );
} else if ( $this->_blog_id > 0 ) {
update_blog_option( $this->_blog_id, $option_name, $this->_options );
} else {
update_option( $option_name, $this->_options, $this->_autoload );
}
}
/**
* Get options keys.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.3
*
* @return string[]
*/
function get_options_keys() {
if ( is_array( $this->_options ) ) {
return array_keys( $this->_options );
} else if ( is_object( $this->_options ) ) {
return array_keys( get_object_vars( $this->_options ) );
}
return array();
}
#--------------------------------------------------------------------------------
#region Migration
#--------------------------------------------------------------------------------
/**
* Migrate options from site level.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*/
function migrate_to_network() {
$site_options = FS_Option_Manager::get_manager($this->_id, true, false);
$options = is_object( $site_options->_options ) ?
get_object_vars( $site_options->_options ) :
$site_options->_options;
if ( ! empty( $options ) ) {
foreach ( $options as $key => $val ) {
$this->set_option( $key, $val, false );
}
$this->store();
}
}
#endregion
#--------------------------------------------------------------------------------
#region Helper Methods
#--------------------------------------------------------------------------------
/**
* @return string
*/
private function get_option_manager_name() {
return $this->_id;
}
#endregion
}