Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce functions to set option autoload value independently in the database #5069

Closed
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
158 changes: 158 additions & 0 deletions src/wp-includes/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,164 @@ function get_options( $options ) {
return $result;
}

/**
* Sets the autoload values for multiple options in the database.
*
* Autoloading too many options can lead to performance problems, especially if the options are not frequently used.
* This function allows modifying the autoload value for multiple options without changing the actual option value.
* This is for example recommended for plugin activation and deactivation hooks, to ensure any options exclusively used
* by the plugin which are generally autoloaded can be set to not autoload when the plugin is inactive.
*
* @since 6.4.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param array $options Associative array of option names and their autoload values to set. The option names are
* expected to not be SQL-escaped. The autoload values accept 'yes'|true to enable or 'no'|false
* to disable.
* @return array Associative array of all provided $options as keys and boolean values for whether their autoload value
* was updated.
*/
function wp_set_option_autoload_values( array $options ) {
global $wpdb;

if ( ! $options ) {
return array();
}

$grouped_options = array(
'yes' => array(),
'no' => array(),
);
$results = array();
foreach ( $options as $option => $autoload ) {
wp_protect_special_option( $option ); // Ensure only valid options can be passed.
if ( 'no' === $autoload || false === $autoload ) { // Sanitize autoload value and categorize accordingly.
$grouped_options['no'][] = $option;
} else {
$grouped_options['yes'][] = $option;
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
}
$results[ $option ] = false; // Initialize result value.
}

$where = array();
$where_args = array();
foreach ( $grouped_options as $autoload => $options ) {
if ( ! $options ) {
continue;
}
$placeholders = trim( str_repeat( '%s,', count( $options ) ), ',' );
$where[] = "autoload != '%s' AND option_name IN ($placeholders)";
$where_args[] = $autoload;
foreach ( $options as $option ) {
$where_args[] = $option;
}
}
$where = 'WHERE ' . implode( ' OR ', $where );

/*
* Determine the relevant options that do not already use the given autoload value.
* If no options are returned, no need to update.
*/
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
$options_to_update = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM $wpdb->options $where", ...$where_args ) );
if ( ! $options_to_update ) {
return $results;
}

// Run UPDATE queries as needed (maximum 2) to update the relevant options' autoload values to 'yes' or 'no'.
foreach ( $grouped_options as $autoload => $options ) {
if ( ! $options ) {
continue;
}
$options = array_intersect( $options, $options_to_update );
$grouped_options[ $autoload ] = $options;
if ( ! $grouped_options[ $autoload ] ) {
continue;
}

// Run query to update autoload value for all the options where it is needed.
$placeholders = trim( str_repeat( '%s,', count( $grouped_options[ $autoload ] ) ), ',' );
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
$success = $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->options SET autoload = %s WHERE option_name IN ($placeholders)", $autoload, ...$grouped_options[ $autoload ] ) );
if ( ! $success ) {
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
// Set option list to an empty array to indicate no options were updated.
$grouped_options[ $autoload ] = array();
continue;
}

// Assume that on success all options were updated, which should be the case given only new values are sent.
foreach ( $grouped_options[ $autoload ] as $option ) {
$results[ $option ] = true;
}
}

/**
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
* If any options were changed to 'yes', delete their individual caches, and delete 'alloptions' cache so that it
* is refreshed as needed.
* If no options were changed to 'yes' but any options were changed to 'no', delete them from the 'alloptions'
* cache. This is not necessary when options were changed to 'yes', since in that situation the entire cache is
* deleted anyway.
*/
if ( $grouped_options['yes'] ) {
wp_cache_delete_multiple( $grouped_options['yes'], 'options' );
wp_cache_delete( 'alloptions', 'options' );
} elseif ( $grouped_options['no'] ) {
$alloptions = wp_load_alloptions( true );
foreach ( $grouped_options['no'] as $option ) {
if ( isset( $alloptions[ $option ] ) ) {
unset( $alloptions[ $option ] );
}
}
wp_cache_set( 'alloptions', $alloptions, 'options' );
}

return $results;
}

/**
* Sets the autoload value for multiple options in the database.
*
* This is a wrapper for {@see wp_set_option_autoload_values()}, which can be used to set different autoload values for
* each option at once.
*
* @since 6.4.0
* @see wp_set_option_autoload_values()
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
*
* @param array $options List of option names. Expected to not be SQL-escaped.
* @param string|bool $autoload Autoload value to control whether to load the options when WordPress starts up.
* Accepts 'yes'|true to enable or 'no'|false to disable.
* @return array Associative array of all provided $options as keys and boolean values for whether their autoload value
* was updated.
*/
function wp_set_options_autoload( array $options, $autoload ) {
return wp_set_option_autoload_values(
array_fill_keys( $options, $autoload )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever! ✨

);
}

/**
* Sets the autoload value for an option in the database.
*
* This is a wrapper for {@see wp_set_option_autoload_values()}, which can be used to set the autoload value for
* multiple options at once.
*
* @since 6.4.0
* @see wp_set_option_autoload_values()
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
*
* @param string $option Name of the option. Expected to not be SQL-escaped.
* @param string|bool $autoload Autoload value to control whether to load the option when WordPress starts up.
* Accepts 'yes'|true to enable or 'no'|false to disable.
* @return bool True if the autoload value was modified, false otherwise.
*/
function wp_set_option_autoload( $option, $autoload ) {
$result = wp_set_option_autoload_values( array( $option => $autoload ) );
if ( isset( $result[ $option ] ) ) {
return $result[ $option ];
}
return false;
}

/**
* Protects WordPress special option from being modified.
*
Expand Down
80 changes: 80 additions & 0 deletions tests/phpunit/tests/option/wpSetOptionAutoload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php
/**
* Test wp_set_option_autoload().
*
* @group option
*
* @covers ::wp_set_option_autoload
*/
class Tests_Option_WpSetOptionAutoload extends WP_UnitTestCase {

/**
* Tests that setting an option's autoload value to 'yes' works as expected.
*
* @ticket 58964
*/
public function test_wp_set_option_autoload_yes() {
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
global $wpdb;

$option = 'test_option';
$value = 'value';

add_option( $option, $value, '', 'no' );

$this->assertTrue( wp_set_option_autoload( $option, 'yes' ), 'Function did not succeed' );
$this->assertSame( 'yes', $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option ) ), 'Option autoload value not updated in database' );
$this->assertFalse( wp_cache_get( $option, 'options' ), 'Option not deleted from individual cache' );
$this->assertFalse( wp_cache_get( 'alloptions', 'options' ), 'Alloptions cache not cleared' );
}

/**
* Tests that setting an option's autoload value to 'no' works as expected.
*
* @ticket 58964
*/
public function test_wp_set_option_autoload_no() {
global $wpdb;

$option = 'test_option';
$value = 'value';

add_option( $option, $value, '', 'yes' );

$this->assertTrue( wp_set_option_autoload( $option, 'no' ), 'Function did not succeed' );
$this->assertSame( 'no', $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option ) ), 'Option autoload value not updated in database' );
$this->assertArrayNotHasKey( $option, wp_cache_get( 'alloptions', 'options' ), 'Option not deleted from alloptions cache' );
}

/**
* Tests that setting an option's autoload value to the same value as prior works as expected.
*
* @ticket 58964
*/
public function test_wp_set_option_autoload_same() {
global $wpdb;

$option = 'test_option';
$value = 'value';

add_option( $option, $value, '', 'yes' );

$this->assertFalse( wp_set_option_autoload( $option, 'yes' ), 'Function did unexpectedly succeed' );
$this->assertSame( 'yes', $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option ) ), 'Option autoload value unexpectedly updated in database' );
}

/**
* Tests that setting a missing option's autoload value does not do anything.
*
* @ticket 58964
*/
public function test_wp_set_option_autoload_missing() {
global $wpdb;

$option = 'test_option';

$this->assertFalse( wp_set_option_autoload( $option, 'yes' ), 'Function did unexpectedly succeed' );
$this->assertNull( $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option ) ), 'Missing option autoload value was set in database' );
$this->assertArrayNotHasKey( $option, wp_cache_get( 'alloptions', 'options' ), 'Missing option found in alloptions cache' );
$this->assertFalse( wp_cache_get( $option, 'options' ), 'Missing option found in individual cache' );
}
}