From 8e5c5fd4bfa241e4ced1846112aa0524671de73f Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Wed, 10 Dec 2025 11:14:05 +0100 Subject: [PATCH 01/19] delay get_option( 'option_optimizer' ) to 'shutdown' --- src/class-plugin.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/class-plugin.php b/src/class-plugin.php index 37c73fe..9830533 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -60,7 +60,7 @@ public static function get_instance() { * @return void */ public function register_hooks() { - $this->accessed_options = \get_option( 'option_optimizer', [ 'used_options' => [] ] )['used_options']; + $this->accessed_options = []; // Hook into all actions and filters to monitor option accesses. // @phpstan-ignore-next-line -- The 'all' hook does not need a return. @@ -135,9 +135,15 @@ public function update_tracked_options() { // Retrieve the existing option_optimizer data. $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); - $option_optimizer['used_options'] = $this->accessed_options; + if ( ! $this->should_reset ) { + foreach ( $this->accessed_options as $option_name => $count ) { + if ( ! isset( $option_optimizer['used_options'][ $option_name ] ) ) { + $option_optimizer['used_options'][ $option_name ] = 0; + } - if ( $this->should_reset ) { + $option_optimizer['used_options'][ $option_name ] += $count; + } + } else { $option_optimizer['used_options'] = []; } From 168ff77e50c3506697e95c6d23ac67295d04349c Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Wed, 10 Dec 2025 15:42:59 +0100 Subject: [PATCH 02/19] already set --- src/class-plugin.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/class-plugin.php b/src/class-plugin.php index 9830533..82a03fb 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -60,8 +60,6 @@ public static function get_instance() { * @return void */ public function register_hooks() { - $this->accessed_options = []; - // Hook into all actions and filters to monitor option accesses. // @phpstan-ignore-next-line -- The 'all' hook does not need a return. \add_filter( 'all', [ $this, 'monitor_option_accesses' ] ); From 0bc9aee610fe2c7833fff8a3ec19c46eb00296df Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Wed, 10 Dec 2025 15:45:12 +0100 Subject: [PATCH 03/19] update data every 5 mins --- src/class-plugin.php | 100 +++++++++++++++++++++++++++++++++++++------ uninstall.php | 3 ++ 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/src/class-plugin.php b/src/class-plugin.php index 82a03fb..db60b33 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -123,6 +123,9 @@ protected function add_option_usage( $option_name ) { /** * Update the 'option_optimizer' option with the list of used options at the end of the page load. * + * Uses transient batching to reduce database writes - only flushes to the main option + * every 5 minutes instead of on every request. + * * @return void */ public function update_tracked_options() { @@ -130,22 +133,95 @@ public function update_tracked_options() { if ( isset( $_GET['page'] ) && $_GET['page'] === 'aaa-option-optimizer' ) { return; } - // Retrieve the existing option_optimizer data. - $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); - if ( ! $this->should_reset ) { - foreach ( $this->accessed_options as $option_name => $count ) { - if ( ! isset( $option_optimizer['used_options'][ $option_name ] ) ) { - $option_optimizer['used_options'][ $option_name ] = 0; - } + // Handle reset: clear batch and main option. + if ( $this->should_reset ) { + \delete_transient( 'option_optimizer_batch' ); - $option_optimizer['used_options'][ $option_name ] += $count; - } - } else { + $option_optimizer = \get_option( 'option_optimizer', [ 'used_options' => [] ] ); $option_optimizer['used_options'] = []; + \update_option( 'option_optimizer', $option_optimizer, false ); + return; + } + + // Get the batch data. + $batch_data = $this->get_batch_data(); + + // Add current request's options to the batch. + foreach ( $this->accessed_options as $option_name => $count ) { + if ( ! isset( $batch_data['options'][ $option_name ] ) ) { + $batch_data['options'][ $option_name ] = 0; + } + $batch_data['options'][ $option_name ] += $count; + } + + // Check if it's time to flush the batch. + $should_flush = ( \time() - $batch_data['last_flush'] ) >= $this->get_flush_interval(); + + // Flush batch to main option every 5 minutes. + if ( ! empty( $batch_data['options'] ) && $should_flush ) { + $this->flush_batch_to_option( $batch_data['options'] ); + + // Reset the batch data. + $batch_data = [ + 'options' => [], + 'last_flush' => \time(), + ]; + } + + // No expiry - batch is explicitly deleted on flush, expiry would only cause data loss. + \set_transient( 'option_optimizer_batch', $batch_data, 0 ); + } + + /** + * Get the batch data. + * + * @return array + */ + protected function get_batch_data() { + // Get existing batch (stores both data and flush timestamp in one transient). + $batch_data = \get_transient( 'option_optimizer_batch' ); + if ( ! \is_array( $batch_data ) || ! isset( $batch_data['options'], $batch_data['last_flush'] ) ) { + $batch_data = [ + 'options' => [], + 'last_flush' => \time(), + ]; + } + + return $batch_data; + } + + /** + * Get the flush interval. + * + * @return int + */ + protected function get_flush_interval() { + return (int) \apply_filters( 'aaa_option_optimizer_flush_interval', 5 * MINUTE_IN_SECONDS ); + } + + /** + * Flush the batched data to the main option_optimizer option. + * + * @param array $batch The batched option usage data. + * + * @return void + */ + protected function flush_batch_to_option( $batch ) { + + if ( empty( $batch ) ) { + return; + } + + $option_optimizer = \get_option( 'option_optimizer', [ 'used_options' => [] ] ); + + foreach ( $batch as $option_name => $count ) { + if ( ! isset( $option_optimizer['used_options'][ $option_name ] ) ) { + $option_optimizer['used_options'][ $option_name ] = 0; + } + $option_optimizer['used_options'][ $option_name ] += $count; } - // Update the 'option_optimizer' option with the new list. - update_option( 'option_optimizer', $option_optimizer, false ); + \update_option( 'option_optimizer', $option_optimizer, false ); } } diff --git a/uninstall.php b/uninstall.php index 4fa17e2..08493b6 100644 --- a/uninstall.php +++ b/uninstall.php @@ -12,5 +12,8 @@ exit; } +// Delete the batch transient. +delete_transient( 'option_optimizer_batch' ); + // Delete the plugin option. delete_option( 'option_optimizer' ); From 7aa92882e85bdc9d580bd5236bf975a500687253 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 11 Dec 2025 10:30:11 +0100 Subject: [PATCH 04/19] hello custom table --- aaa-option-optimizer.php | 49 +++++++--- src/class-database.php | 200 +++++++++++++++++++++++++++++++++++++++ src/class-plugin.php | 40 ++------ src/class-rest.php | 15 ++- uninstall.php | 11 ++- 5 files changed, 259 insertions(+), 56 deletions(-) create mode 100644 src/class-database.php diff --git a/aaa-option-optimizer.php b/aaa-option-optimizer.php index ef82da5..190a272 100644 --- a/aaa-option-optimizer.php +++ b/aaa-option-optimizer.php @@ -27,12 +27,19 @@ register_deactivation_hook( __FILE__, 'aaa_option_optimizer_deactivation' ); /** - * Activation hooked function to store start stats. + * Activation hooked function to store start stats and create table. * * @return void */ function aaa_option_optimizer_activation() { global $wpdb; + + // Create the custom table. + Emilia\OptionOptimizer\Database::create_table(); + + // Migrate existing data if present. + Emilia\OptionOptimizer\Database::maybe_migrate(); + $autoload_values = \wp_autoload_values_to_autoload(); $placeholders = implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) ); @@ -42,16 +49,19 @@ function aaa_option_optimizer_activation() { ); // phpcs:enable WordPress.DB - update_option( - 'option_optimizer', - [ - 'starting_point_kb' => ( $result->autoload_size / 1024 ), - 'starting_point_num' => $result->count, - 'starting_point_date' => current_time( 'mysql' ), - 'used_options' => [], - ], - false - ); + // Only set starting point if not already set (preserve existing data). + $existing = get_option( 'option_optimizer' ); + if ( empty( $existing['starting_point_date'] ) ) { + update_option( + 'option_optimizer', + [ + 'starting_point_kb' => ( $result->autoload_size / 1024 ), + 'starting_point_num' => $result->count, + 'starting_point_date' => current_time( 'mysql' ), + ], + false + ); + } } /** @@ -64,6 +74,23 @@ function aaa_option_optimizer_deactivation() { update_option( 'option_optimizer', $aaa_option_value, false ); } +/** + * Ensure database table exists and migrate data if needed. + * Runs on plugins_loaded to handle existing installs that don't trigger activation. + * + * @return void + */ +function aaa_option_optimizer_maybe_upgrade() { + // Check if table exists, create if not. + if ( ! Emilia\OptionOptimizer\Database::table_exists() ) { + Emilia\OptionOptimizer\Database::create_table(); + } + + // Migrate existing data if present. + Emilia\OptionOptimizer\Database::maybe_migrate(); +} +add_action( 'plugins_loaded', 'aaa_option_optimizer_maybe_upgrade' ); + /** * Initializes the plugin. * diff --git a/src/class-database.php b/src/class-database.php new file mode 100644 index 0000000..c4ff0e2 --- /dev/null +++ b/src/class-database.php @@ -0,0 +1,200 @@ +prefix . self::TABLE_NAME; + } + + /** + * Create the custom table. + * + * @return void + */ + public static function create_table() { + global $wpdb; + + $table_name = self::get_table_name(); + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE {$table_name} ( + option_name VARCHAR(191) NOT NULL, + access_count BIGINT UNSIGNED DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (option_name) + ) {$charset_collate};"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + \dbDelta( $sql ); + } + + /** + * Drop the custom table. + * + * @return void + */ + public static function drop_table() { + global $wpdb; + + $table_name = self::get_table_name(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe (from constant). + $wpdb->query( "DROP TABLE IF EXISTS {$table_name}" ); + } + + /** + * Check if the table exists. + * + * @return bool + */ + public static function table_exists() { + global $wpdb; + + $table_name = self::get_table_name(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name; + } + + /** + * Migrate data from the old option format to the custom table. + * + * @return void + */ + public static function maybe_migrate() { + $option_data = \get_option( 'option_optimizer' ); + + // No data or already migrated (no used_options key). + if ( ! \is_array( $option_data ) || ! isset( $option_data['used_options'] ) ) { + return; + } + + // Ensure table exists. + if ( ! self::table_exists() ) { + self::create_table(); + } + + // Batch insert old data to custom table. + if ( ! empty( $option_data['used_options'] ) ) { + self::batch_insert( $option_data['used_options'] ); + } + + // Remove used_options from the option, keep metadata. + unset( $option_data['used_options'] ); + \update_option( 'option_optimizer', $option_data, false ); + } + + /** + * Batch insert or update option counts. + * + * @param array $options Array of option_name => count. + * + * @return void + */ + public static function batch_insert( $options ) { + global $wpdb; + + if ( empty( $options ) ) { + return; + } + + $table_name = self::get_table_name(); + $values = []; + $placeholders = []; + + foreach ( $options as $option_name => $count ) { + $placeholders[] = '(%s, %d, NOW())'; + $values[] = $option_name; + $values[] = (int) $count; + } + + $sql = "INSERT INTO {$table_name} (option_name, access_count, created_at) + VALUES " . implode( ', ', $placeholders ) . ' + ON DUPLICATE KEY UPDATE access_count = access_count + VALUES(access_count)'; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( $wpdb->prepare( $sql, ...$values ) ); + } + + /** + * Get all tracked options as an associative array. + * + * @return array Array of option_name => access_count. + */ + public static function get_tracked_options() { + global $wpdb; + + $table_name = self::get_table_name(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe (from constant). + $results = $wpdb->get_results( "SELECT option_name, access_count FROM {$table_name}", ARRAY_A ); + + if ( empty( $results ) ) { + return []; + } + + $options = []; + foreach ( $results as $row ) { + $options[ $row['option_name'] ] = (int) $row['access_count']; + } + + return $options; + } + + /** + * Get tracked option names as a keyed array for efficient lookups. + * + * @return array Array of option_name => true. + */ + public static function get_tracked_option_keys() { + global $wpdb; + + $table_name = self::get_table_name(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe (from constant). + $option_names = $wpdb->get_col( "SELECT option_name FROM {$table_name}" ); + + if ( empty( $option_names ) ) { + return []; + } + + return array_fill_keys( $option_names, true ); + } + + /** + * Clear all tracked options from the table. + * + * @return void + */ + public static function clear_tracked_options() { + global $wpdb; + + $table_name = self::get_table_name(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe (from constant). + $wpdb->query( "TRUNCATE TABLE {$table_name}" ); + } +} diff --git a/src/class-plugin.php b/src/class-plugin.php index db60b33..b8413f1 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -121,9 +121,9 @@ protected function add_option_usage( $option_name ) { } /** - * Update the 'option_optimizer' option with the list of used options at the end of the page load. + * Update the tracked options at the end of the page load. * - * Uses transient batching to reduce database writes - only flushes to the main option + * Uses transient batching to reduce database writes - only flushes to the custom table * every 5 minutes instead of on every request. * * @return void @@ -134,13 +134,10 @@ public function update_tracked_options() { return; } - // Handle reset: clear batch and main option. + // Handle reset: clear batch and custom table. if ( $this->should_reset ) { \delete_transient( 'option_optimizer_batch' ); - - $option_optimizer = \get_option( 'option_optimizer', [ 'used_options' => [] ] ); - $option_optimizer['used_options'] = []; - \update_option( 'option_optimizer', $option_optimizer, false ); + Database::clear_tracked_options(); return; } @@ -158,9 +155,9 @@ public function update_tracked_options() { // Check if it's time to flush the batch. $should_flush = ( \time() - $batch_data['last_flush'] ) >= $this->get_flush_interval(); - // Flush batch to main option every 5 minutes. + // Flush batch to custom table every 5 minutes. if ( ! empty( $batch_data['options'] ) && $should_flush ) { - $this->flush_batch_to_option( $batch_data['options'] ); + Database::batch_insert( $batch_data['options'] ); // Reset the batch data. $batch_data = [ @@ -199,29 +196,4 @@ protected function get_batch_data() { protected function get_flush_interval() { return (int) \apply_filters( 'aaa_option_optimizer_flush_interval', 5 * MINUTE_IN_SECONDS ); } - - /** - * Flush the batched data to the main option_optimizer option. - * - * @param array $batch The batched option usage data. - * - * @return void - */ - protected function flush_batch_to_option( $batch ) { - - if ( empty( $batch ) ) { - return; - } - - $option_optimizer = \get_option( 'option_optimizer', [ 'used_options' => [] ] ); - - foreach ( $batch as $option_name => $count ) { - if ( ! isset( $option_optimizer['used_options'][ $option_name ] ) ) { - $option_optimizer['used_options'][ $option_name ] = 0; - } - $option_optimizer['used_options'][ $option_name ] += $count; - } - - \update_option( 'option_optimizer', $option_optimizer, false ); - } } diff --git a/src/class-rest.php b/src/class-rest.php index 9310487..9aaf07d 100644 --- a/src/class-rest.php +++ b/src/class-rest.php @@ -229,9 +229,8 @@ public function get_unused_options() { global $wpdb; - // Load used options from option_optimizer. - $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); - $used_options = $option_optimizer['used_options']; + // Load used options from custom table. + $used_options = Database::get_tracked_option_keys(); $query = " SELECT option_name @@ -335,9 +334,8 @@ public function get_used_not_autoloaded_options() { global $wpdb; - // Load used options from option_optimizer. - $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); - $used_options = $option_optimizer['used_options']; + // Load used options from custom table (with counts). + $used_options = Database::get_tracked_options(); if ( empty( $used_options ) ) { return new \WP_REST_Response( @@ -460,9 +458,8 @@ public function get_options_that_do_not_exist() { global $wpdb; - // Load used options. - $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); - $used_options = $option_optimizer['used_options']; + // Load used options from custom table (with counts). + $used_options = Database::get_tracked_options(); if ( empty( $used_options ) ) { return new \WP_REST_Response( diff --git a/uninstall.php b/uninstall.php index 08493b6..b954086 100644 --- a/uninstall.php +++ b/uninstall.php @@ -2,9 +2,9 @@ /** * Uninstall the plugin. * - * Delete the plugin option. + * Delete the plugin option and custom table. * - * @package Progress_Planner + * @package Emilia\OptionOptimizer */ // If uninstall not called from WordPress, then exit. @@ -12,6 +12,13 @@ exit; } +global $wpdb; + +// Drop the custom table. +$aaa_option_optimizer_table = $wpdb->prefix . 'option_optimizer_tracked'; +// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe (from constant prefix). +$wpdb->query( "DROP TABLE IF EXISTS {$aaa_option_optimizer_table}" ); + // Delete the batch transient. delete_transient( 'option_optimizer_batch' ); From fdcfd13fe599c3b82be96284561eb387b01a0b02 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Fri, 12 Dec 2025 09:59:17 +0100 Subject: [PATCH 05/19] change namespace --- aaa-option-optimizer.php | 14 +++++++------- src/autoload.php | 4 ++-- src/class-admin-page.php | 4 ++-- src/class-database.php | 4 ++-- src/class-map-plugin-to-options.php | 6 +++--- src/class-plugin.php | 4 ++-- src/class-rest.php | 4 ++-- uninstall.php | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/aaa-option-optimizer.php b/aaa-option-optimizer.php index 190a272..cfd6bf9 100644 --- a/aaa-option-optimizer.php +++ b/aaa-option-optimizer.php @@ -2,7 +2,7 @@ /** * Plugin that tracks autoloaded options usage and allows the user to optimize them. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer * * Plugin Name: AAA Option Optimizer * Plugin URI: https://joost.blog/plugins/aaa-option-optimizer/ @@ -35,10 +35,10 @@ function aaa_option_optimizer_activation() { global $wpdb; // Create the custom table. - Emilia\OptionOptimizer\Database::create_table(); + Progress_Planner\OptionOptimizer\Database::create_table(); // Migrate existing data if present. - Emilia\OptionOptimizer\Database::maybe_migrate(); + Progress_Planner\OptionOptimizer\Database::maybe_migrate(); $autoload_values = \wp_autoload_values_to_autoload(); $placeholders = implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) ); @@ -82,12 +82,12 @@ function aaa_option_optimizer_deactivation() { */ function aaa_option_optimizer_maybe_upgrade() { // Check if table exists, create if not. - if ( ! Emilia\OptionOptimizer\Database::table_exists() ) { - Emilia\OptionOptimizer\Database::create_table(); + if ( ! Progress_Planner\OptionOptimizer\Database::table_exists() ) { + Progress_Planner\OptionOptimizer\Database::create_table(); } // Migrate existing data if present. - Emilia\OptionOptimizer\Database::maybe_migrate(); + Progress_Planner\OptionOptimizer\Database::maybe_migrate(); } add_action( 'plugins_loaded', 'aaa_option_optimizer_maybe_upgrade' ); @@ -97,7 +97,7 @@ function aaa_option_optimizer_maybe_upgrade() { * @return void */ function aaa_option_optimizer_init() { - $optimizer = new Emilia\OptionOptimizer\Plugin(); + $optimizer = new Progress_Planner\OptionOptimizer\Plugin(); $optimizer->register_hooks(); } diff --git a/src/autoload.php b/src/autoload.php index 502fda2..4f04541 100644 --- a/src/autoload.php +++ b/src/autoload.php @@ -2,12 +2,12 @@ /** * Autoload PHP classes for the plugin. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ spl_autoload_register( function ( $class_name ) { - $prefix = 'Emilia\\OptionOptimizer\\'; + $prefix = 'Progress_Planner\\OptionOptimizer\\'; if ( 0 !== \strpos( $class_name, $prefix ) ) { return; diff --git a/src/class-admin-page.php b/src/class-admin-page.php index ea476fe..aecc969 100644 --- a/src/class-admin-page.php +++ b/src/class-admin-page.php @@ -2,10 +2,10 @@ /** * Admin page functionality for AAA Option Optimizer. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ -namespace Emilia\OptionOptimizer; +namespace Progress_Planner\OptionOptimizer; /** * Admin page functionality for AAA Option Optimizer. diff --git a/src/class-database.php b/src/class-database.php index c4ff0e2..7244047 100644 --- a/src/class-database.php +++ b/src/class-database.php @@ -2,10 +2,10 @@ /** * Database functionality for AAA Option Optimizer. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ -namespace Emilia\OptionOptimizer; +namespace Progress_Planner\OptionOptimizer; /** * Handles custom database table for tracking options. diff --git a/src/class-map-plugin-to-options.php b/src/class-map-plugin-to-options.php index 2732f9a..799d8fe 100644 --- a/src/class-map-plugin-to-options.php +++ b/src/class-map-plugin-to-options.php @@ -2,15 +2,15 @@ /** * Functionality to map options to plugins. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ -namespace Emilia\OptionOptimizer; +namespace Progress_Planner\OptionOptimizer; /** * Class Map_Plugin_To_Options * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ class Map_Plugin_To_Options { /** diff --git a/src/class-plugin.php b/src/class-plugin.php index b8413f1..63c3a7c 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -2,10 +2,10 @@ /** * Plugin functionality for AAA Option Optimizer. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ -namespace Emilia\OptionOptimizer; +namespace Progress_Planner\OptionOptimizer; /** * Core functionality of AAA Option Optimizer. diff --git a/src/class-rest.php b/src/class-rest.php index 9aaf07d..f1a550a 100644 --- a/src/class-rest.php +++ b/src/class-rest.php @@ -2,10 +2,10 @@ /** * REST functionality for AAA Option Optimizer. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ -namespace Emilia\OptionOptimizer; +namespace Progress_Planner\OptionOptimizer; use WP_Error; use WP_REST_Request; diff --git a/uninstall.php b/uninstall.php index b954086..05bf9fe 100644 --- a/uninstall.php +++ b/uninstall.php @@ -4,7 +4,7 @@ * * Delete the plugin option and custom table. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ // If uninstall not called from WordPress, then exit. From fddd0dc03e61a5e3090afcea9d308eb515519a6a Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Fri, 12 Dec 2025 10:02:15 +0100 Subject: [PATCH 06/19] update links and phpcs prefix --- README.md | 14 +++++++------- composer.json | 2 +- phpcs.xml.dist | 2 +- readme.txt | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7692f71..463dd10 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -[![CS](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/cs.yml/badge.svg)](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/cs.yml) -[![PHPStan](https://github.com/Emilia-Capital/aaa-option-optimizer/actions/workflows/phpstan.yml/badge.svg)](https://github.com/Emilia-Capital/aaa-option-optimizer/actions/workflows/phpstan.yml) -[![Lint](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/lint.yml/badge.svg)](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/lint.yml) -[![Security](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/security.yml/badge.svg)](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/security.yml) +[![CS](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/cs.yml/badge.svg)](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/cs.yml) +[![PHPStan](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/phpstan.yml/badge.svg)](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/phpstan.yml) +[![Lint](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/lint.yml/badge.svg)](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/lint.yml) +[![Security](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/security.yml/badge.svg)](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/security.yml) [![WordPress Plugin Version](https://img.shields.io/wordpress/plugin/v/aaa-option-optimizer.svg)](https://wordpress.org/plugins/aaa-option-optimizer/) ![WordPress Plugin: Tested WP Version](https://img.shields.io/wordpress/plugin/tested/aaa-option-optimizer.svg) @@ -10,7 +10,7 @@ [![WordPress Plugin Rating](https://img.shields.io/wordpress/plugin/stars/aaa-option-optimizer.svg)](https://wordpress.org/support/plugin/aaa-option-optimizer/reviews/) [![GitHub](https://img.shields.io/github/license/ProgressPlanner/aaa-option-optimizer.svg)](https://github.com/ProgressPlanner/aaa-option-optimizer/blob/main/LICENSE) -[![Try this plugin on the WordPress playground](https://img.shields.io/badge/Try%20this%20plugin%20on%20the%20WordPress%20Playground-%23117AC9.svg?style=for-the-badge&logo=WordPress&logoColor=ddd)](https://playground.wordpress.net/#%7B%22landingPage%22:%22/wp-admin/tools.php?page=aaa-option-optimizer%22,%22features%22:%7B%22networking%22:true%7D,%22steps%22:%5B%7B%22step%22:%22defineWpConfigConsts%22,%22consts%22:%7B%22IS_PLAYGROUND_PREVIEW%22:true%7D%7D,%7B%22step%22:%22login%22,%22username%22:%22admin%22,%22password%22:%22password%22%7D,%7B%22step%22:%22installPlugin%22,%22pluginZipFile%22:%7B%22resource%22:%22url%22,%22url%22:%22https://bypass-cors.altha.workers.dev/https://github.com/Emilia-Capital/aaa-option-optimizer/archive/refs/heads/develop.zip%22%7D,%22options%22:%7B%22activate%22:true%7D%7D%5D%7D) +[![Try this plugin on the WordPress playground](https://img.shields.io/badge/Try%20this%20plugin%20on%20the%20WordPress%20Playground-%23117AC9.svg?style=for-the-badge&logo=WordPress&logoColor=ddd)](https://playground.wordpress.net/#%7B%22landingPage%22:%22/wp-admin/tools.php?page=aaa-option-optimizer%22,%22features%22:%7B%22networking%22:true%7D,%22steps%22:%5B%7B%22step%22:%22defineWpConfigConsts%22,%22consts%22:%7B%22IS_PLAYGROUND_PREVIEW%22:true%7D%7D,%7B%22step%22:%22login%22,%22username%22:%22admin%22,%22password%22:%22password%22%7D,%7B%22step%22:%22installPlugin%22,%22pluginZipFile%22:%7B%22resource%22:%22url%22,%22url%22:%22https://bypass-cors.altha.workers.dev/https://github.com/ProgressPlanner/aaa-option-optimizer/archive/refs/heads/develop.zip%22%7D,%22options%22:%7B%22activate%22:true%7D%7D%5D%7D) ![GitHub banner](/.wordpress-org/github_banner_aaaoo_pp.png) @@ -26,7 +26,7 @@ Install this plugin, and go through your entire site. Best is to use it normally ### Why the AAA prefix in the plugin name? -Because the plugin needs to measure options being loaded, it benefits from being loaded itself first. As WordPress loads plugins alphabetically, +Because the plugin needs to measure options being loaded, it benefits from being loaded itself first. As WordPress loads plugins alphabetically, starting the name with AAA made sense. ### Do I need to take precautions? @@ -35,7 +35,7 @@ Yes!! Backup your database. ### How can I add recognized plugins? -Please do a pull request via GitHub on [this file](https://github.com/Emilia-Capital/aaa-option-optimizer/blob/develop/known-plugins/known-plugins.json) in the plugin. +Please do a pull request via GitHub on [this file](https://github.com/ProgressPlanner/aaa-option-optimizer/blob/develop/known-plugins/known-plugins.json) in the plugin. ### How can I report security bugs? diff --git a/composer.json b/composer.json index 68a8d8f..1dd06e8 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "emilia/aaa-option-optimizer", + "name": "progress-planner/aaa-option-optimizer", "description": "Plugin that tracks autoloaded options usage and allows the user to optimize them.", "type": "wordpress-plugin", "license": "GPL-3.0-or-later", diff --git a/phpcs.xml.dist b/phpcs.xml.dist index fffbfd5..902af51 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -108,7 +108,7 @@ - + diff --git a/readme.txt b/readme.txt index aaa4514..74301c3 100644 --- a/readme.txt +++ b/readme.txt @@ -29,7 +29,7 @@ Yes!! Backup your database. = Where can I report bugs? = -Please use [our GitHub](https://github.com/emilia-Capital/aaa-option-optimizer/) for reporting bugs or making code suggestions. Feel free to use the forums for asking questions too, of course. +Please use [our GitHub](https://github.com/ProgressPlanner/aaa-option-optimizer/) for reporting bugs or making code suggestions. Feel free to use the forums for asking questions too, of course. For security issues, please see the next question. @@ -39,7 +39,7 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro = How can I add recognized plugins? = -Please do a pull request via GitHub on [this file](https://github.com/Emilia-Capital/aaa-option-optimizer/blob/develop/known-plugins/known-plugins.json) in the plugin. +Please do a pull request via GitHub on [this file](https://github.com/ProgressPlanner/aaa-option-optimizer/blob/develop/known-plugins/known-plugins.json) in the plugin. == Installation == 1. Search for AAA Option Optimizer on the repository. @@ -109,7 +109,7 @@ Implement the missing functionality to create an option with value `false` when = 1.1 = The plugin now recognizes plugins from which the options came (thanks to a great pull by [Rogier Lankhorst](https://profiles.wordpress.org/rogierlankhorst/)). If you're a plugin developer and want your plugin's options -properly recognized, please do a pull request [on this file](https://github.com/Emilia-Capital/aaa-option-optimizer/blob/main/known-plugins/known-plugins.json). +properly recognized, please do a pull request [on this file](https://github.com/ProgressPlanner/aaa-option-optimizer/blob/main/known-plugins/known-plugins.json). Small enhancements: From ee94c4661717b11521f1518853c7c594e5ed5366 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 18 Dec 2025 15:19:12 +0100 Subject: [PATCH 07/19] avoid PHP fatal error in case of downgrade --- src/class-database.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/class-database.php b/src/class-database.php index 7244047..8c0748e 100644 --- a/src/class-database.php +++ b/src/class-database.php @@ -102,8 +102,9 @@ public static function maybe_migrate() { self::batch_insert( $option_data['used_options'] ); } - // Remove used_options from the option, keep metadata. - unset( $option_data['used_options'] ); + // Set used_options to an empty array, so we avoid php fatal error in case user decides to downgrade the plugin. + $option_data['used_options'] = []; + \update_option( 'option_optimizer', $option_data, false ); } From 1272145672d5f75a971c1136280aa95200bd0948 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 18 Dec 2025 15:25:21 +0100 Subject: [PATCH 08/19] adjust migration condition --- src/class-database.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/class-database.php b/src/class-database.php index 8c0748e..b12805f 100644 --- a/src/class-database.php +++ b/src/class-database.php @@ -88,7 +88,7 @@ public static function maybe_migrate() { $option_data = \get_option( 'option_optimizer' ); // No data or already migrated (no used_options key). - if ( ! \is_array( $option_data ) || ! isset( $option_data['used_options'] ) ) { + if ( ! \is_array( $option_data ) || empty( $option_data['used_options'] ) ) { return; } @@ -98,9 +98,7 @@ public static function maybe_migrate() { } // Batch insert old data to custom table. - if ( ! empty( $option_data['used_options'] ) ) { - self::batch_insert( $option_data['used_options'] ); - } + self::batch_insert( $option_data['used_options'] ); // Set used_options to an empty array, so we avoid php fatal error in case user decides to downgrade the plugin. $option_data['used_options'] = []; From bc6dc26b8a78b245566758d6e556dadab4db25d1 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Fri, 19 Dec 2025 08:41:13 +0100 Subject: [PATCH 09/19] composer.lock --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index 68b99dc..d186fc9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b719f200626be0650504e5dddce6534d", + "content-hash": "68e73cf23844dd66f4d8b3b14eef46cd", "packages": [], "packages-dev": [ { From ad54e71309722c60022f3765b37bf80f67733ead Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Fri, 19 Dec 2025 08:42:19 +0100 Subject: [PATCH 10/19] one more prefix for phpcs --- phpcs.xml.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 17af8a0..7c4ebd2 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -109,6 +109,7 @@ + From 435c60b2a4dcf9fb0ebda1e55599211f4c601ac7 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Fri, 19 Dec 2025 08:43:20 +0100 Subject: [PATCH 11/19] now fix indend phpcs errors --- aaa-option-optimizer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aaa-option-optimizer.php b/aaa-option-optimizer.php index ccf7b13..e2b59fc 100644 --- a/aaa-option-optimizer.php +++ b/aaa-option-optimizer.php @@ -59,8 +59,8 @@ function aaa_option_optimizer_activation() { 'starting_point_num' => $result->count, 'starting_point_date' => current_time( 'mysql' ), 'settings' => [ - 'option_tracking' => 'pre_option', - ], + 'option_tracking' => 'pre_option', + ], ], false ); From c813a6e3e34a688c91e132edd2707fb6f634963a Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Fri, 19 Dec 2025 13:48:48 +0100 Subject: [PATCH 12/19] split into chunks --- src/class-database.php | 43 +++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/class-database.php b/src/class-database.php index b12805f..7a865bd 100644 --- a/src/class-database.php +++ b/src/class-database.php @@ -109,33 +109,46 @@ public static function maybe_migrate() { /** * Batch insert or update option counts. * - * @param array $options Array of option_name => count. + * Splits large datasets into chunks and wraps them in a transaction + * for optimal performance on slow hosts with large datasets. + * + * @param array $options Array of option_name => count. + * @param int $chunk_size Number of options per query. Default 500. * * @return void */ - public static function batch_insert( $options ) { + public static function batch_insert( $options, $chunk_size = 500 ) { global $wpdb; if ( empty( $options ) ) { return; } - $table_name = self::get_table_name(); - $values = []; - $placeholders = []; + $table_name = self::get_table_name(); - foreach ( $options as $option_name => $count ) { - $placeholders[] = '(%s, %d, NOW())'; - $values[] = $option_name; - $values[] = (int) $count; - } + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->query( 'BEGIN' ); + + foreach ( array_chunk( $options, $chunk_size, true ) as $chunk ) { + $values = []; + $placeholders = []; - $sql = "INSERT INTO {$table_name} (option_name, access_count, created_at) - VALUES " . implode( ', ', $placeholders ) . ' - ON DUPLICATE KEY UPDATE access_count = access_count + VALUES(access_count)'; + foreach ( $chunk as $option_name => $count ) { + $placeholders[] = '(%s, %d, NOW())'; + $values[] = $option_name; + $values[] = (int) $count; + } - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( $wpdb->prepare( $sql, ...$values ) ); + $sql = "INSERT INTO {$table_name} (option_name, access_count, created_at) + VALUES " . implode( ', ', $placeholders ) . ' + ON DUPLICATE KEY UPDATE access_count = access_count + VALUES(access_count)'; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( $wpdb->prepare( $sql, ...$values ) ); + } + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->query( 'COMMIT' ); } /** From 39307c64edb8a65ecc679cca7a4ebde939102112 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Mon, 22 Dec 2025 12:49:32 +0100 Subject: [PATCH 13/19] remove batch write, not needed with custom tables --- src/class-plugin.php | 62 +++----------------------------------------- 1 file changed, 4 insertions(+), 58 deletions(-) diff --git a/src/class-plugin.php b/src/class-plugin.php index cd7b7e8..8961968 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -144,9 +144,6 @@ protected function add_option_usage( $option_name ) { /** * Update the tracked options at the end of the page load. * - * Uses transient batching to reduce database writes - only flushes to the custom table - * every 5 minutes instead of on every request. - * * @return void */ public function update_tracked_options() { @@ -155,66 +152,15 @@ public function update_tracked_options() { return; } - // Handle reset: clear batch and custom table. + // Handle reset. if ( $this->should_reset ) { - \delete_transient( 'option_optimizer_batch' ); Database::clear_tracked_options(); return; } - // Get the batch data. - $batch_data = $this->get_batch_data(); - - // Add current request's options to the batch. - foreach ( $this->accessed_options as $option_name => $count ) { - if ( ! isset( $batch_data['options'][ $option_name ] ) ) { - $batch_data['options'][ $option_name ] = 0; - } - $batch_data['options'][ $option_name ] += $count; - } - - // Check if it's time to flush the batch. - $should_flush = ( \time() - $batch_data['last_flush'] ) >= $this->get_flush_interval(); - - // Flush batch to custom table every 5 minutes. - if ( ! empty( $batch_data['options'] ) && $should_flush ) { - Database::batch_insert( $batch_data['options'] ); - - // Reset the batch data. - $batch_data = [ - 'options' => [], - 'last_flush' => \time(), - ]; + // Write accessed options directly to the custom table. + if ( ! empty( $this->accessed_options ) ) { + Database::batch_insert( $this->accessed_options ); } - - // No expiry - batch is explicitly deleted on flush, expiry would only cause data loss. - \set_transient( 'option_optimizer_batch', $batch_data, 0 ); - } - - /** - * Get the batch data. - * - * @return array - */ - protected function get_batch_data() { - // Get existing batch (stores both data and flush timestamp in one transient). - $batch_data = \get_transient( 'option_optimizer_batch' ); - if ( ! \is_array( $batch_data ) || ! isset( $batch_data['options'], $batch_data['last_flush'] ) ) { - $batch_data = [ - 'options' => [], - 'last_flush' => \time(), - ]; - } - - return $batch_data; - } - - /** - * Get the flush interval. - * - * @return int - */ - protected function get_flush_interval() { - return (int) \apply_filters( 'aaa_option_optimizer_flush_interval', 5 * MINUTE_IN_SECONDS ); } } From 90ea8bcf188620ab1c1ec7dfd6f1c99eeb0fef7a Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Mon, 22 Dec 2025 12:56:26 +0100 Subject: [PATCH 14/19] fix docblock and phpstan error --- src/class-plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/class-plugin.php b/src/class-plugin.php index 8961968..0caf091 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -19,9 +19,9 @@ class Plugin { public static $instance; /** - * Holds the names of the options accessed during the request. + * Holds the options accessed during the request with their access counts. * - * @var string[] + * @var array */ protected $accessed_options = []; From 3e06c2fa0a44b6ca4fe4b197e11f50e259a6350c Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Mon, 22 Dec 2025 14:04:16 +0100 Subject: [PATCH 15/19] prevent race conditions / sqlite lock errors --- aaa-option-optimizer.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aaa-option-optimizer.php b/aaa-option-optimizer.php index e2b59fc..c1153f0 100644 --- a/aaa-option-optimizer.php +++ b/aaa-option-optimizer.php @@ -84,6 +84,11 @@ function aaa_option_optimizer_deactivation() { * @return void */ function aaa_option_optimizer_maybe_upgrade() { + // Only run on admin pages, not on AJAX or REST requests to avoid race conditions. + if ( ! is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { + return; + } + // Check if table exists, create if not. if ( ! Progress_Planner\OptionOptimizer\Database::table_exists() ) { Progress_Planner\OptionOptimizer\Database::create_table(); From c8e4b9d0979ea0cda8a76e8872685523da2a727d Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Mon, 22 Dec 2025 14:33:03 +0100 Subject: [PATCH 16/19] put back, for backwards compat --- aaa-option-optimizer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/aaa-option-optimizer.php b/aaa-option-optimizer.php index c1153f0..6072705 100644 --- a/aaa-option-optimizer.php +++ b/aaa-option-optimizer.php @@ -58,6 +58,7 @@ function aaa_option_optimizer_activation() { 'starting_point_kb' => ( $result->autoload_size / 1024 ), 'starting_point_num' => $result->count, 'starting_point_date' => current_time( 'mysql' ), + 'used_options' => [], // For backward compatibility. 'settings' => [ 'option_tracking' => 'pre_option', ], From ae2daaed7299dd7a9325b198dfeba4ad7bf20264 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Mon, 22 Dec 2025 14:33:36 +0100 Subject: [PATCH 17/19] prevent triggering migration in parallel --- src/class-database.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/class-database.php b/src/class-database.php index 7a865bd..5a8cab6 100644 --- a/src/class-database.php +++ b/src/class-database.php @@ -92,6 +92,12 @@ public static function maybe_migrate() { return; } + // Prevent concurrent migrations. + if ( \get_transient( 'aaa_option_optimizer_migrating' ) ) { + return; + } + \set_transient( 'aaa_option_optimizer_migrating', true, 60 ); + // Ensure table exists. if ( ! self::table_exists() ) { self::create_table(); @@ -104,6 +110,8 @@ public static function maybe_migrate() { $option_data['used_options'] = []; \update_option( 'option_optimizer', $option_data, false ); + + \delete_transient( 'aaa_option_optimizer_migrating' ); } /** From 491bf068d4c3fd7764789eb369933f3fab0784c3 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Mon, 22 Dec 2025 15:05:19 +0100 Subject: [PATCH 18/19] add ajax migration --- aaa-option-optimizer.php | 6 +-- js/admin-script.js | 70 +++++++++++++++++++++++++++++++ src/class-admin-page.php | 41 ++++++++++++++++-- src/class-database.php | 89 +++++++++++++++++++++++++++++++++------- src/class-rest.php | 22 ++++++++++ 5 files changed, 207 insertions(+), 21 deletions(-) diff --git a/aaa-option-optimizer.php b/aaa-option-optimizer.php index 6072705..a4ce593 100644 --- a/aaa-option-optimizer.php +++ b/aaa-option-optimizer.php @@ -79,8 +79,9 @@ function aaa_option_optimizer_deactivation() { } /** - * Ensure database table exists and migrate data if needed. + * Ensure database table exists. * Runs on plugins_loaded to handle existing installs that don't trigger activation. + * Migration is handled via AJAX on the plugin admin page. * * @return void */ @@ -94,9 +95,6 @@ function aaa_option_optimizer_maybe_upgrade() { if ( ! Progress_Planner\OptionOptimizer\Database::table_exists() ) { Progress_Planner\OptionOptimizer\Database::create_table(); } - - // Migrate existing data if present. - Progress_Planner\OptionOptimizer\Database::maybe_migrate(); } add_action( 'plugins_loaded', 'aaa_option_optimizer_maybe_upgrade' ); diff --git a/js/admin-script.js b/js/admin-script.js index 45cfd25..3531a73 100644 --- a/js/admin-script.js +++ b/js/admin-script.js @@ -631,4 +631,74 @@ jQuery( document ).ready( function () { initializeDataTable( selector ); } } ); + + // Migration functionality. + jQuery( '#aaa-start-migration' ).on( 'click', function ( e ) { + e.preventDefault(); + const button = jQuery( this ); + const progressContainer = jQuery( '#aaa-migration-progress' ); + const progressBar = jQuery( '#aaa-migration-progress-bar' ); + const statusText = jQuery( '#aaa-migration-status' ); + const total = aaaOptionOptimizer.migration.total; + + button.prop( 'disabled', true ); + progressContainer.show(); + statusText.text( aaaOptionOptimizer.i18n.migrating ); + + /** + * Performs a single migration chunk via AJAX. + */ + function migrateChunk() { + jQuery.ajax( { + url: + aaaOptionOptimizer.root + + 'aaa-option-optimizer/v1/migrate', + method: 'POST', + beforeSend: ( xhr ) => + xhr.setRequestHeader( + 'X-WP-Nonce', + aaaOptionOptimizer.nonce + ), + success( response ) { + if ( ! response.success ) { + statusText.text( + response.message || + aaaOptionOptimizer.i18n.migrationError + ); + button.prop( 'disabled', false ); + return; + } + + const migrated = total - response.remaining; + const percent = Math.round( ( migrated / total ) * 100 ); + + progressBar.css( 'width', percent + '%' ); + statusText.text( + aaaOptionOptimizer.i18n.migratedOf + .replace( '%1$d', migrated ) + .replace( '%2$d', total ) + ); + + if ( response.remaining > 0 ) { + // Continue with next chunk. + migrateChunk(); + } else { + // Migration complete. + statusText.text( + aaaOptionOptimizer.i18n.migrationComplete + ); + setTimeout( function () { + window.location.reload(); + }, 1000 ); + } + }, + error() { + statusText.text( aaaOptionOptimizer.i18n.migrationError ); + button.prop( 'disabled', false ); + }, + } ); + } + + migrateChunk(); + } ); } ); diff --git a/src/class-admin-page.php b/src/class-admin-page.php index 05833c3..7b7a6a7 100644 --- a/src/class-admin-page.php +++ b/src/class-admin-page.php @@ -189,9 +189,10 @@ public function enqueue_scripts( $hook ) { 'aaa-option-optimizer-admin-js', 'aaaOptionOptimizer', [ - 'root' => \esc_url_raw( \rest_url() ), - 'nonce' => \wp_create_nonce( 'wp_rest' ), - 'i18n' => [ + 'root' => \esc_url_raw( \rest_url() ), + 'nonce' => \wp_create_nonce( 'wp_rest' ), + 'migration' => Database::get_migration_status(), + 'i18n' => [ 'filterBySource' => \esc_html__( 'Filter by source', 'aaa-option-optimizer' ), 'showValue' => \esc_html__( 'Show', 'aaa-option-optimizer' ), 'addAutoload' => \esc_html__( 'Add autoload', 'aaa-option-optimizer' ), @@ -207,6 +208,11 @@ public function enqueue_scripts( $hook ) { 'apply' => \esc_html__( 'Apply', 'aaa-option-optimizer' ), 'search' => \esc_html__( 'Search:', 'aaa-option-optimizer' ), + 'migrating' => \esc_html__( 'Migrating...', 'aaa-option-optimizer' ), + 'migrationComplete' => \esc_html__( 'Migration complete! Reloading page...', 'aaa-option-optimizer' ), + 'migrationError' => \esc_html__( 'Migration error. Please try again.', 'aaa-option-optimizer' ), + /* translators: %1$d: number of migrated options, %2$d: total number of options */ + 'migratedOf' => \esc_html__( 'Migrated %1$d of %2$d options', 'aaa-option-optimizer' ), 'entries' => [ '_' => \esc_html__( 'entries', 'aaa-option-optimizer' ), '1' => \esc_html__( 'entry', 'aaa-option-optimizer' ), @@ -260,10 +266,39 @@ public function render_admin_page_ajax() { $wpdb->prepare( "SELECT count(*) AS count, SUM( LENGTH( option_value ) ) as autoload_size FROM {$wpdb->options} WHERE autoload IN ( $placeholders )", $autoload_values ) ); // phpcs:enable WordPress.DB + + // Check if migration is needed. + $migration_status = Database::get_migration_status(); ?>

+ +
+

+
+ +

+ +

+ +

+
+ +

diff --git a/src/class-database.php b/src/class-database.php index 5a8cab6..16c5793 100644 --- a/src/class-database.php +++ b/src/class-database.php @@ -80,38 +80,99 @@ public static function table_exists() { } /** - * Migrate data from the old option format to the custom table. + * Number of options to migrate per request. * - * @return void + * @var int */ - public static function maybe_migrate() { + const MIGRATION_CHUNK_SIZE = 1000; + + /** + * Get migration status. + * + * @return array{needs_migration: bool, total: int, remaining: int} + */ + public static function get_migration_status() { $option_data = \get_option( 'option_optimizer' ); - // No data or already migrated (no used_options key). if ( ! \is_array( $option_data ) || empty( $option_data['used_options'] ) ) { - return; + return [ + 'needs_migration' => false, + 'total' => 0, + 'remaining' => 0, + ]; } - // Prevent concurrent migrations. - if ( \get_transient( 'aaa_option_optimizer_migrating' ) ) { - return; + $remaining = \count( $option_data['used_options'] ); + + // Get total from transient or set it on first check. + $total = \get_transient( 'aaa_option_optimizer_migration_total' ); + if ( false === $total ) { + $total = $remaining; + \set_transient( 'aaa_option_optimizer_migration_total', $total, HOUR_IN_SECONDS ); + } + + return [ + 'needs_migration' => true, + 'total' => (int) $total, + 'remaining' => $remaining, + ]; + } + + /** + * Migrate a chunk of data from the old option format to the custom table. + * + * Processes in chunks to avoid timeouts on slow hosts with large datasets. + * + * @return array{success: bool, remaining: int, total: int} + */ + public static function migrate_chunk() { + $option_data = \get_option( 'option_optimizer' ); + + // No data or already migrated. + if ( ! \is_array( $option_data ) || empty( $option_data['used_options'] ) ) { + \delete_transient( 'aaa_option_optimizer_migration_total' ); + return [ + 'success' => true, + 'remaining' => 0, + 'total' => 0, + ]; } - \set_transient( 'aaa_option_optimizer_migrating', true, 60 ); // Ensure table exists. if ( ! self::table_exists() ) { self::create_table(); } - // Batch insert old data to custom table. - self::batch_insert( $option_data['used_options'] ); + // Get total for progress tracking. + $total = \get_transient( 'aaa_option_optimizer_migration_total' ); + if ( false === $total ) { + $total = \count( $option_data['used_options'] ); + \set_transient( 'aaa_option_optimizer_migration_total', $total, HOUR_IN_SECONDS ); + } + + // Take a chunk of options to migrate. + $chunk = \array_slice( $option_data['used_options'], 0, self::MIGRATION_CHUNK_SIZE, true ); + + // Batch insert chunk to custom table. + self::batch_insert( $chunk ); - // Set used_options to an empty array, so we avoid php fatal error in case user decides to downgrade the plugin. - $option_data['used_options'] = []; + // Remove migrated options from the array. + $option_data['used_options'] = \array_slice( $option_data['used_options'], self::MIGRATION_CHUNK_SIZE, null, true ); \update_option( 'option_optimizer', $option_data, false ); - \delete_transient( 'aaa_option_optimizer_migrating' ); + $remaining = \count( $option_data['used_options'] ); + + // Clean up total transient when done. + if ( 0 === $remaining ) { + \delete_transient( 'aaa_option_optimizer_migration_total' ); + } + + return [ + 'success' => true, + 'remaining' => $remaining, + 'total' => (int) $total, + ]; } /** diff --git a/src/class-rest.php b/src/class-rest.php index 36c39ba..472eea4 100644 --- a/src/class-rest.php +++ b/src/class-rest.php @@ -181,6 +181,18 @@ public function register_rest_routes() { }, ] ); + + \register_rest_route( + 'aaa-option-optimizer/v1', + '/migrate', + [ + 'methods' => 'POST', + 'callback' => [ $this, 'migrate_chunk' ], + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + ] + ); } /** @@ -193,6 +205,16 @@ public function reset_stats() { return new \WP_REST_Response( [ 'success' => true ], 200 ); } + /** + * Migrate a chunk of data from old format to custom table. + * + * @return \WP_REST_Response + */ + public function migrate_chunk() { + $result = Database::migrate_chunk(); + return new \WP_REST_Response( $result, 200 ); + } + /** * Update autoload status of an option. * From 75ec11daf16dee7eac942d34e7557e697c1235a3 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Mon, 22 Dec 2025 15:33:21 +0100 Subject: [PATCH 19/19] phpstan fixes --- aaa-option-optimizer.php | 3 --- src/class-admin-page.php | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/aaa-option-optimizer.php b/aaa-option-optimizer.php index a4ce593..057afeb 100644 --- a/aaa-option-optimizer.php +++ b/aaa-option-optimizer.php @@ -37,9 +37,6 @@ function aaa_option_optimizer_activation() { // Create the custom table. Progress_Planner\OptionOptimizer\Database::create_table(); - // Migrate existing data if present. - Progress_Planner\OptionOptimizer\Database::maybe_migrate(); - $autoload_values = \wp_autoload_values_to_autoload(); $placeholders = implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) ); diff --git a/src/class-admin-page.php b/src/class-admin-page.php index 7b7a6a7..e45a243 100644 --- a/src/class-admin-page.php +++ b/src/class-admin-page.php @@ -281,7 +281,7 @@ public function render_admin_page_ajax() { printf( /* translators: %d: number of options to migrate */ \esc_html__( 'We need to migrate %d tracked options to the new database format.', 'aaa-option-optimizer' ), - \esc_html( $migration_status['remaining'] ) + (int) $migration_status['remaining'] ); ?>