From ea90c3842a921573e35734cc7f7a3033d88f16c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Thu, 16 Oct 2025 22:43:41 -0300 Subject: [PATCH 01/19] Added --where flag to finely select rows aimed for replacement Added --revision / --no-revision Added --callback Reroll wp-cli#104, wp-cli#128, wp-cli#184 Fixes wp-cli#125, wp-cli#127, wp-cli#142 --- features/search-replace-new-options.feature | 513 ++++++++++++++++++++ src/Search_Replace_Command.php | 155 +++++- src/WP_CLI/SearchReplacer.php | 38 +- 3 files changed, 673 insertions(+), 33 deletions(-) create mode 100644 features/search-replace-new-options.feature diff --git a/features/search-replace-new-options.feature b/features/search-replace-new-options.feature new file mode 100644 index 00000000..c2c918a9 --- /dev/null +++ b/features/search-replace-new-options.feature @@ -0,0 +1,513 @@ +Feature: Test new search-replace options (--callback, --revisions, --where) + + @require-mysql + Scenario: Search replace with callback function + Given a WP install + And a callback-function.php file: + """ + ] + * : Perform the replacement on specific rows. Use semi-colon to + * specify multiple specifictions. + * format: [,table]:[column,...]:quoted-SQL-condition + * + * [--revisions] + * : Default true. + * If false, then identical to --where='posts::post_status="publish";postmeta::post_id IN (SELECT ID FROM {posts} WHERE post_status="publish")' + * * [--precise] * : Force the use of PHP (instead of SQL) which is more thorough, - * but slower. + * but slower. If --callback is specified, --precise is inferred. * * [--recurse-objects] * : Enable recursing into objects to replace strings. Defaults to true; @@ -193,6 +212,9 @@ class Search_Replace_Command extends WP_CLI_Command { * [--verbose] * : Prints rows to the console as they're updated. * + * [--callback=] + * : Runs a user-specified function on each string that contains . is passed as the second argument and the regex string as the third if it exists: call_user_func( 'callback', $data, $new, $search_regex ). + * * [--regex] * : Runs the search using a regular expression (without delimiters). * Warning: search-replace will take about 15-20x longer when using --regex. @@ -268,10 +290,10 @@ public function __invoke( $args, $assoc_args ) { $this->dry_run = Utils\get_flag_value( $assoc_args, 'dry-run', false ); $php_only = Utils\get_flag_value( $assoc_args, 'precise', false ); $this->recurse_objects = Utils\get_flag_value( $assoc_args, 'recurse-objects', true ); + $this->callback = Utils\get_flag_value( $assoc_args, 'callback', false ); $this->verbose = Utils\get_flag_value( $assoc_args, 'verbose', false ); $this->format = Utils\get_flag_value( $assoc_args, 'format' ); $this->regex = Utils\get_flag_value( $assoc_args, 'regex', false ); - $default_regex_delimiter = false; if ( null !== $this->regex ) { @@ -317,12 +339,30 @@ public function __invoke( $args, $assoc_args ) { $this->skip_columns = explode( ',', Utils\get_flag_value( $assoc_args, 'skip-columns', '' ) ); $this->skip_tables = explode( ',', Utils\get_flag_value( $assoc_args, 'skip-tables', '' ) ); $this->include_columns = array_filter( explode( ',', Utils\get_flag_value( $assoc_args, 'include-columns', '' ) ) ); + $this->where = $this->develop_where_specs( Utils\get_flag_value( $assoc_args, 'where' ) ); + $revisions = Utils\get_flag_value( $assoc_args, 'revisions', true ); + if ( ! $revisions ) { + $this->no_revision(); + } if ( $old === $new && ! $this->regex ) { WP_CLI::warning( "Replacement value '{$old}' is identical to search value '{$new}'. Skipping operation." ); exit; } + if ( $this->callback ) { + // We must load WordPress as the function may depend on it. + WP_CLI::get_runner()->load_wordpress(); + if ( ! function_exists( $this->callback ) ) { + WP_CLI::error( 'The callback function does not exist. Skipping operation.' ); + } + + if ( false === $php_only ) { + WP_CLI::error( 'PHP is required to execute a callback function. --no-precise cannot be set.' ); + } + $php_only = true; + } + $export = Utils\get_flag_value( $assoc_args, 'export' ); if ( null !== $export ) { if ( $this->dry_run ) { @@ -413,6 +453,28 @@ public function __invoke( $args, $assoc_args ) { // Get table names based on leftover $args or supplied $assoc_args $tables = Utils\wp_get_table_names( $args, $assoc_args ); + // If a custom `where` conditions were passed, then exclude other tables from processing. + if ( $this->where ) { + $tables = array_intersect( $tables, array_keys( $this->where ) ); + $columns = []; + foreach ( array_values( $this->where ) as $_ => $cols ) { + $columns = array_merge( $columns, array_keys( $cols ) ); + } + if ( $columns ) { + if ( in_array( '*', $columns ) ) { + $columns = array_filter( + $columns, + function ( $e ) { + return $e !== '*'; + } + ); + if ( $this->include_columns ) { + WP_CLI::warning( 'Column-catch was passed to --where while. But --include-columns will still restrict replacements to columns: ' . implode( ',', $this->include_columns ) ); + } + } + $this->include_columns = array_merge( $this->include_columns, $columns ); + } + } foreach ( $tables as $table ) { @@ -467,6 +529,8 @@ public function __invoke( $args, $assoc_args ) { continue; } + $clauses = $this->get_clauses( $table, $col ); + if ( $this->verbose && 'count' !== $this->format ) { $this->start_time = microtime( true ); WP_CLI::log( sprintf( 'Checking: %s.%s', $table, $col ) ); @@ -478,8 +542,9 @@ public function __invoke( $args, $assoc_args ) { $col_sql = self::esc_sql_ident( $col ); $wpdb->last_error = ''; + $where = $clauses ? ' AND ' . implode( ' AND ', $clauses ) : ''; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident - $serial_row = $wpdb->get_row( "SELECT * FROM $table_sql WHERE $col_sql REGEXP '^[aiO]:[1-9]' LIMIT 1" ); + $serial_row = $wpdb->get_row( "SELECT * FROM $table_sql WHERE $col_sql REGEXP '^[aiO]:[1-9]' $where LIMIT 1" ); // When the regex triggers an error, we should fall back to PHP if ( false !== strpos( $wpdb->last_error, 'ERROR 1139' ) ) { @@ -489,10 +554,10 @@ public function __invoke( $args, $assoc_args ) { if ( $php_only || $this->regex || null !== $serial_row ) { $type = 'PHP'; - $count = $this->php_handle_col( $col, $primary_keys, $table, $old, $new ); + $count = $this->php_handle_col( $col, $primary_keys, $table, $old, $new, $clauses ); } else { $type = 'SQL'; - $count = $this->sql_handle_col( $col, $primary_keys, $table, $old, $new ); + $count = $this->sql_handle_col( $col, $primary_keys, $table, $old, $new, $clauses ); } if ( $this->report && ( $count || ! $this->report_changed_only ) ) { @@ -563,7 +628,15 @@ private function php_export_table( $table, $old, $new ) { foreach ( $all_columns as $col ) { $value = $row->$col; if ( $value && ! in_array( $col, $primary_keys, true ) && ! in_array( $col, $this->skip_columns, true ) ) { - $new_value = $replacer->run( $value ); + $new_value = $replacer->run( + $value, + false, + [ + 'table' => $table, + 'col' => $col, + 'key' => $primary_keys, + ] + ); if ( $new_value !== $value ) { ++$col_counts[ $col ]; $value = $new_value; @@ -596,24 +669,26 @@ private function php_export_table( $table, $old, $new ) { return array( $table_report, $total_rows ); } - private function sql_handle_col( $col, $primary_keys, $table, $old, $new ) { + private function sql_handle_col( $col, $primary_keys, $table, $old, $new, $clauses ) { global $wpdb; $table_sql = self::esc_sql_ident( $table ); $col_sql = self::esc_sql_ident( $col ); if ( $this->dry_run ) { if ( $this->log_handle ) { - $count = $this->log_sql_diff( $col, $primary_keys, $table, $old, $new ); + $count = $this->log_sql_diff( $col, $primary_keys, $table, $old, $new, $clauses ); } else { + $where = $clauses ? ' AND ' . implode( ' AND ', $clauses ) : ''; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident - $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT($col_sql) FROM $table_sql WHERE $col_sql LIKE BINARY %s;", '%' . self::esc_like( $old ) . '%' ) ); + $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT($col_sql) FROM $table_sql WHERE $col_sql LIKE BINARY %s $where;", '%' . self::esc_like( $old ) . '%' ) ); } } else { if ( $this->log_handle ) { - $this->log_sql_diff( $col, $primary_keys, $table, $old, $new ); + $this->log_sql_diff( $col, $primary_keys, $table, $old, $new, $clauses ); } + $where = $clauses ? ' WHERE ' . implode( ' AND ', $clauses ) : ''; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident - $count = $wpdb->query( $wpdb->prepare( "UPDATE $table_sql SET $col_sql = REPLACE($col_sql, %s, %s);", $old, $new ) ); + $count = $wpdb->query( $wpdb->prepare( "UPDATE $table_sql SET $col_sql = REPLACE($col_sql, %s, %s) $where;", $old, $new ) ); } if ( $this->verbose && 'table' === $this->format ) { @@ -623,22 +698,21 @@ private function sql_handle_col( $col, $primary_keys, $table, $old, $new ) { return $count; } - private function php_handle_col( $col, $primary_keys, $table, $old, $new ) { + private function php_handle_col( $col, $primary_keys, $table, $old, $new, $additional_where ) { global $wpdb; $count = 0; - $replacer = new SearchReplacer( $old, $new, $this->recurse_objects, $this->regex, $this->regex_flags, $this->regex_delimiter, null !== $this->log_handle, $this->regex_limit ); + $replacer = new SearchReplacer( $old, $new, $this->recurse_objects, $this->regex, $this->regex_flags, $this->regex_delimiter, null !== $this->log_handle, $this->regex_limit, $this->callback ); $table_sql = self::esc_sql_ident( $table ); $col_sql = self::esc_sql_ident( $col ); - $base_key_condition = ''; - $where_key = ''; + $base_key_condition = $additional_where; if ( ! $this->regex ) { - $base_key_condition = "$col_sql" . $wpdb->prepare( ' LIKE BINARY %s', '%' . self::esc_like( $old ) . '%' ); - $where_key = "WHERE $base_key_condition"; + $base_key_condition[] = "$col_sql" . $wpdb->prepare( ' LIKE BINARY %s', '%' . self::esc_like( $old ) . '%' ); } + $where_key = $base_key_condition ? ' WHERE ' . implode( ' AND ', $base_key_condition ) : ''; $escaped_primary_keys = self::esc_sql_ident( $primary_keys ); $primary_keys_sql = implode( ',', $escaped_primary_keys ); $order_by_keys = array_map( @@ -726,13 +800,8 @@ static function ( $key ) { $next_key_conditions[] = '( ' . implode( ' AND ', $next_key_subconditions ) . ' )'; } - $where_key_conditions = array(); - if ( $base_key_condition ) { - $where_key_conditions[] = $base_key_condition; - } - $where_key_conditions[] = '( ' . implode( ' OR ', $next_key_conditions ) . ' )'; - - $where_key = 'WHERE ' . implode( ' AND ', $where_key_conditions ); + $base_key_condition[] = '( ' . implode( ' OR ', $next_key_conditions ) . ' )'; + $where_key = 'WHERE ' . implode( ' AND ', $base_key_condition ); } if ( $this->verbose && 'table' === $this->format ) { @@ -955,7 +1024,7 @@ private function get_colors( $assoc_args, $colors ) { * @param string $new New value to replace the old value with. * @return int Count of changed rows. */ - private function log_sql_diff( $col, $primary_keys, $table, $old, $new ) { + private function log_sql_diff( $col, $primary_keys, $table, $old, $new, $clauses ) { global $wpdb; if ( $primary_keys ) { $esc_primary_keys = implode( ', ', self::esc_sql_ident( $primary_keys ) ); @@ -966,9 +1035,10 @@ private function log_sql_diff( $col, $primary_keys, $table, $old, $new ) { $table_sql = self::esc_sql_ident( $table ); $col_sql = self::esc_sql_ident( $col ); + $where_sql = $clauses ? ' AND ' . implode( ' AND ', $clauses ) : ''; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident - $results = $wpdb->get_results( $wpdb->prepare( "SELECT {$primary_keys_sql}{$col_sql} FROM {$table_sql} WHERE {$col_sql} LIKE BINARY %s", '%' . self::esc_like( $old ) . '%' ), ARRAY_N ); + $results = $wpdb->get_results( $wpdb->prepare( "SELECT {$primary_keys_sql}{$col_sql} FROM {$table_sql} WHERE {$col_sql} LIKE BINARY %s {$where_sql}", '%' . self::esc_like( $old ) . '%' ), ARRAY_N ); if ( empty( $results ) ) { return 0; @@ -1149,4 +1219,37 @@ private function log_write( $col, $keys, $table, $old_bits, $new_bits ) { fwrite( $this->log_handle, "{$table_column_id_log}\n{$old_log}\n{$new_log}\n" ); } + + public function develop_where_specs( $str_specs ) { + global $wpdb; + $specs = array_filter( explode( ';', $str_specs ) ); + $clauses = []; + foreach ( $specs as $spec ) { + list( $tables, $cols, $conditions ) = explode( ':', $spec, 3 ); + $tables = array_filter( explode( ',', $tables ) ); + $cols = array_filter( explode( ',', $cols ) ) ? : [ '*' ]; + foreach ( $tables as $table ) { + foreach ( $cols as $col ) { + $clauses[ empty( $wpdb->{$table} ) ? $table : $wpdb->{$table} ][ $col ][] = $conditions; + } + } + } + + return $clauses; + } + + public function no_revision() { + global $wpdb; + $this->where[ $wpdb->posts ]['*'][] = self::esc_sql_ident( 'post_status' ) . '=' . self::esc_sql_value( 'publish' ); + $this->where[ $wpdb->postmeta ]['*'][] = self::esc_sql_ident( 'post_id' ) . ' IN ( SELECT ID FROM ' . $wpdb->posts . ' WHERE ' . self::esc_sql_ident( 'post_status' ) . '=' . self::esc_sql_value( 'publish' ) . ')'; + } + + public function get_clauses( $table, $column = null ) { + return array_filter( + array_merge( + $this->where[ $table ][ $column ] ?? [], + $this->where[ $table ]['*'] ?? [] + ) + ); + } } diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index 8c5ee951..a4bb8108 100644 --- a/src/WP_CLI/SearchReplacer.php +++ b/src/WP_CLI/SearchReplacer.php @@ -67,7 +67,7 @@ class SearchReplacer { * @param bool $logging Whether logging. * @param integer $regex_limit The maximum possible replacements for each pattern in each subject string. */ - public function __construct( $from, $to, $recurse_objects = false, $regex = false, $regex_flags = '', $regex_delimiter = '/', $logging = false, $regex_limit = -1 ) { + public function __construct( $from, $to, $recurse_objects = false, $regex = false, $regex_flags = '', $regex_delimiter = '/', $logging = false, $regex_limit = -1, $callback = false ) { $this->from = $from; $this->to = $to; $this->recurse_objects = $recurse_objects; @@ -76,6 +76,7 @@ public function __construct( $from, $to, $recurse_objects = false, $regex = fals $this->regex_delimiter = $regex_delimiter; $this->regex_limit = $regex_limit; $this->logging = $logging; + $this->callback = $callback; $this->clear_log_data(); // Get the XDebug nesting level. Will be zero (no limit) if no value is set @@ -92,15 +93,15 @@ public function __construct( $from, $to, $recurse_objects = false, $regex = fals * * @return array The original array with all elements replaced as needed. */ - public function run( $data, $serialised = false ) { - return $this->run_recursively( $data, $serialised ); + public function run( $data, $serialised = false, $opts = [] ) { + return $this->run_recursively( $data, $serialised, 0, [], $opts ); } /** * @param int $recursion_level Current recursion depth within the original data. * @param array $visited_data Data that has been seen in previous recursion iterations. */ - private function run_recursively( $data, $serialised, $recursion_level = 0, $visited_data = array() ) { + private function run_recursively( $data, $serialised, $recursion_level = 0, $visited_data = array(), $opts = [] ) { // some unseriliased data cannot be re-serialised eg. SimpleXMLElements try { @@ -192,7 +193,11 @@ private function run_recursively( $data, $serialised, $recursion_level = 0, $vis $search_regex .= $this->regex_delimiter; $search_regex .= $this->regex_flags; - $result = preg_replace( $search_regex, $this->to, $data, $this->regex_limit ); + if ( $this->callback ) { + $result = \call_user_func( $this->callback, $data, $this->to, $search_regex, $opts ); + } else { + $result = preg_replace( $search_regex, $this->to, $data, $this->regex_limit ); + } if ( null === $result || PREG_NO_ERROR !== preg_last_error() ) { \WP_CLI::warning( sprintf( @@ -201,10 +206,29 @@ private function run_recursively( $data, $serialised, $recursion_level = 0, $vis ) ); } - $data = $result; + } elseif ( $this->callback ) { + if ( strpos( $data, $this->from ) !== false ) { + $result = \call_user_func( $this->callback, $data, $this->to, $opts ); + } else { + // We can skip calling the function here. It must still be set so we don't remove text. + $result = $data; + } } else { - $data = str_replace( $this->from, $this->to, $data ); + $result = str_replace( $this->from, $this->to, $data ); + } + + if ( $this->callback ) { + if ( false === $result ) { + WP_CLI::error( 'The callback function return false. Stopping operation.' ); + } elseif ( is_wp_error( $result ) ) { + $message = $errors->get_error_message(); + WP_CLI::error( 'The callback function threw an error. Stopping operation. ' . $message ); + } elseif ( ! is_string( $result ) ) { + WP_CLI::error( 'The callback function did not return a string. Stopping operation.' ); + } } + + $data = $result; if ( $this->logging && $old_data !== $data ) { $this->log_data[] = $old_data; } From 0071c869a415f8208c4f8f87f5a0a7cfcbbe27dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Sun, 19 Oct 2025 00:55:29 -0300 Subject: [PATCH 02/19] phpcbf + features split --- features/search-replace-callback.feature | 129 ++++++++++ features/search-replace-revisions.feature | 106 ++++++++ ...s.feature => search-replace-where.feature} | 235 +----------------- src/Search_Replace_Command.php | 26 +- 4 files changed, 249 insertions(+), 247 deletions(-) create mode 100644 features/search-replace-callback.feature create mode 100644 features/search-replace-revisions.feature rename features/{search-replace-new-options.feature => search-replace-where.feature} (55%) diff --git a/features/search-replace-callback.feature b/features/search-replace-callback.feature new file mode 100644 index 00000000..cc1df240 --- /dev/null +++ b/features/search-replace-callback.feature @@ -0,0 +1,129 @@ +Feature: Test search-replace --callback option + + @require-mysql + Scenario: Search replace with callback function + Given a WP install + And a callback-function.php file: + """ + dry_run = Utils\get_flag_value( $assoc_args, 'dry-run', false ); - $php_only = Utils\get_flag_value( $assoc_args, 'precise', false ); - $this->recurse_objects = Utils\get_flag_value( $assoc_args, 'recurse-objects', true ); - $this->callback = Utils\get_flag_value( $assoc_args, 'callback', false ); - $this->verbose = Utils\get_flag_value( $assoc_args, 'verbose', false ); - $this->format = Utils\get_flag_value( $assoc_args, 'format' ); - $this->regex = Utils\get_flag_value( $assoc_args, 'regex', false ); + $old = array_shift( $args ); + $new = array_shift( $args ); + $total = 0; + $report = array(); + $this->dry_run = Utils\get_flag_value( $assoc_args, 'dry-run', false ); + $php_only = Utils\get_flag_value( $assoc_args, 'precise', false ); + $this->recurse_objects = Utils\get_flag_value( $assoc_args, 'recurse-objects', true ); + $this->callback = Utils\get_flag_value( $assoc_args, 'callback', false ); + $this->verbose = Utils\get_flag_value( $assoc_args, 'verbose', false ); + $this->format = Utils\get_flag_value( $assoc_args, 'format' ); + $this->regex = Utils\get_flag_value( $assoc_args, 'regex', false ); $default_regex_delimiter = false; if ( null !== $this->regex ) { @@ -461,11 +461,11 @@ public function __invoke( $args, $assoc_args ) { $columns = array_merge( $columns, array_keys( $cols ) ); } if ( $columns ) { - if ( in_array( '*', $columns ) ) { + if ( in_array( '*', $columns, true ) ) { $columns = array_filter( $columns, function ( $e ) { - return $e !== '*'; + return '*' !== $e; } ); if ( $this->include_columns ) { From 82cbe76d437bc2727428726678e2f6413caa34ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Sun, 19 Oct 2025 01:03:25 -0300 Subject: [PATCH 03/19] phpstan --- src/WP_CLI/SearchReplacer.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index a4bb8108..4c8e17b3 100644 --- a/src/WP_CLI/SearchReplacer.php +++ b/src/WP_CLI/SearchReplacer.php @@ -57,6 +57,11 @@ class SearchReplacer { */ private $max_recursion; + /** + * @var bool + */ + private $callback; + /** * @param string $from String we're looking to replace. * @param string $to What we want it to be replaced with. @@ -219,12 +224,12 @@ private function run_recursively( $data, $serialised, $recursion_level = 0, $vis if ( $this->callback ) { if ( false === $result ) { - WP_CLI::error( 'The callback function return false. Stopping operation.' ); + \WP_CLI::error( 'The callback function return false. Stopping operation.' ); } elseif ( is_wp_error( $result ) ) { $message = $errors->get_error_message(); - WP_CLI::error( 'The callback function threw an error. Stopping operation. ' . $message ); + \WP_CLI::error( 'The callback function threw an error. Stopping operation. ' . $message ); } elseif ( ! is_string( $result ) ) { - WP_CLI::error( 'The callback function did not return a string. Stopping operation.' ); + \WP_CLI::error( 'The callback function did not return a string. Stopping operation.' ); } } From faef60ff1ff43ff8957000f9b26d8049e852a193 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 4 Nov 2025 19:10:28 +0100 Subject: [PATCH 04/19] PHPStan fix --- src/Search_Replace_Command.php | 2 +- src/WP_CLI/SearchReplacer.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index a94afd3f..fa518aa2 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -120,7 +120,7 @@ class Search_Replace_Command extends WP_CLI_Command { private $start_time; /** - * @var string|false + * @var array>|false */ private $where; diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index 4c8e17b3..76602944 100644 --- a/src/WP_CLI/SearchReplacer.php +++ b/src/WP_CLI/SearchReplacer.php @@ -226,7 +226,7 @@ private function run_recursively( $data, $serialised, $recursion_level = 0, $vis if ( false === $result ) { \WP_CLI::error( 'The callback function return false. Stopping operation.' ); } elseif ( is_wp_error( $result ) ) { - $message = $errors->get_error_message(); + $message = $result->get_error_message(); \WP_CLI::error( 'The callback function threw an error. Stopping operation. ' . $message ); } elseif ( ! is_string( $result ) ) { \WP_CLI::error( 'The callback function did not return a string. Stopping operation.' ); From e84713815c43b9c960485284d963cae72f6485b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Tue, 11 Nov 2025 14:57:04 -0300 Subject: [PATCH 05/19] Update src/Search_Replace_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Search_Replace_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index fa518aa2..c963dca0 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -469,7 +469,7 @@ function ( $e ) { } ); if ( $this->include_columns ) { - WP_CLI::warning( 'Column-catch was passed to --where while. But --include-columns will still restrict replacements to columns: ' . implode( ',', $this->include_columns ) ); + WP_CLI::warning( 'Column wildcard (*) was passed to --where. But --include-columns will still restrict replacements to columns: ' . implode( ',', $this->include_columns ) ); } } $this->include_columns = array_merge( $this->include_columns, $columns ); From 76c2296aa665293f49fd06f7bb7118aa66a5fbc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Tue, 11 Nov 2025 14:58:00 -0300 Subject: [PATCH 06/19] Update src/Search_Replace_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Search_Replace_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index c963dca0..8bb0bc04 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -194,7 +194,7 @@ class Search_Replace_Command extends WP_CLI_Command { * * [--where=] * : Perform the replacement on specific rows. Use semi-colon to - * specify multiple specifictions. + * specify multiple specifications. * format:
[,table]:[column,...]:quoted-SQL-condition * * [--revisions] From c4fe56cbc5fccf3cab3afeb7422cc96976ede0f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Tue, 11 Nov 2025 14:58:19 -0300 Subject: [PATCH 07/19] Update src/Search_Replace_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Search_Replace_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index 8bb0bc04..88868a16 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -1238,7 +1238,7 @@ public function develop_where_specs( $str_specs ) { return $clauses; } - public function no_revision() { + private function no_revision() { global $wpdb; $this->where[ $wpdb->posts ]['*'][] = self::esc_sql_ident( 'post_status' ) . '=' . self::esc_sql_value( 'publish' ); $this->where[ $wpdb->postmeta ]['*'][] = self::esc_sql_ident( 'post_id' ) . ' IN ( SELECT ID FROM ' . $wpdb->posts . ' WHERE ' . self::esc_sql_ident( 'post_status' ) . '=' . self::esc_sql_value( 'publish' ) . ')'; From 735fedfa5c77c79b8eb49253a0f2412d64622a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Tue, 11 Nov 2025 14:58:38 -0300 Subject: [PATCH 08/19] Update src/Search_Replace_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Search_Replace_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index 88868a16..ffb85e15 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -1220,7 +1220,7 @@ private function log_write( $col, $keys, $table, $old_bits, $new_bits ) { fwrite( $this->log_handle, "{$table_column_id_log}\n{$old_log}\n{$new_log}\n" ); } - public function develop_where_specs( $str_specs ) { + private function develop_where_specs( $str_specs ) { global $wpdb; $specs = array_filter( explode( ';', $str_specs ) ); $clauses = []; From cf89ef581fee3fc2deba7fa3687c28623a312361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Tue, 11 Nov 2025 14:58:53 -0300 Subject: [PATCH 09/19] Update src/Search_Replace_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Search_Replace_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index ffb85e15..3219ea52 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -1244,7 +1244,7 @@ private function no_revision() { $this->where[ $wpdb->postmeta ]['*'][] = self::esc_sql_ident( 'post_id' ) . ' IN ( SELECT ID FROM ' . $wpdb->posts . ' WHERE ' . self::esc_sql_ident( 'post_status' ) . '=' . self::esc_sql_value( 'publish' ) . ')'; } - public function get_clauses( $table, $column = null ) { + private function get_clauses( $table, $column = null ) { return array_filter( array_merge( $this->where[ $table ][ $column ] ?? [], From 8b45d7e495d25da06c8e8a1e0e3870d7802d5cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Tue, 11 Nov 2025 15:01:35 -0300 Subject: [PATCH 10/19] Update src/WP_CLI/SearchReplacer.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/WP_CLI/SearchReplacer.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index 76602944..7264b394 100644 --- a/src/WP_CLI/SearchReplacer.php +++ b/src/WP_CLI/SearchReplacer.php @@ -58,7 +58,9 @@ class SearchReplacer { private $max_recursion; /** - * @var bool + * @var callable|string|bool Callback function or method to be invoked during search/replace operations. + * If set to a callable or a string (function name), this will be called for each replacement. + * If false, no callback is used. */ private $callback; From eb1cc917d093a8267c27b7bf086ca6c703bb2080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Tue, 11 Nov 2025 15:02:54 -0300 Subject: [PATCH 11/19] Update src/Search_Replace_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Search_Replace_Command.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index 3219ea52..6d53035a 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -120,6 +120,12 @@ class Search_Replace_Command extends WP_CLI_Command { private $start_time; /** + * WHERE clause specifications for filtering rows during search-replace operations. + * + * The array is structured as [table_name => [column_name => [values]]], where each + * table name maps to an array of column names, each of which maps to an array of + * string values to match in the WHERE clause. If set to false, no filtering is applied. + * * @var array>|false */ private $where; From 21c0b81839d1ad5243ab13efd12463c6df70469b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Tue, 11 Nov 2025 15:10:53 -0300 Subject: [PATCH 12/19] Update src/WP_CLI/SearchReplacer.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/WP_CLI/SearchReplacer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index 7264b394..80e9b5af 100644 --- a/src/WP_CLI/SearchReplacer.php +++ b/src/WP_CLI/SearchReplacer.php @@ -215,7 +215,7 @@ private function run_recursively( $data, $serialised, $recursion_level = 0, $vis } } elseif ( $this->callback ) { if ( strpos( $data, $this->from ) !== false ) { - $result = \call_user_func( $this->callback, $data, $this->to, $opts ); + $result = \call_user_func( $this->callback, $data, $this->to, null, $opts ); } else { // We can skip calling the function here. It must still be set so we don't remove text. $result = $data; From 05041ab4da57455bfd17a5de91baa07e00f4c9c6 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 11 Nov 2025 23:05:18 +0100 Subject: [PATCH 13/19] Add some docblocks --- src/Search_Replace_Command.php | 111 +++++++++++++++++++++++++++------ src/WP_CLI/SearchReplacer.php | 16 +++-- 2 files changed, 103 insertions(+), 24 deletions(-) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index 6d53035a..508c0137 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -611,6 +611,14 @@ function ( $e ) { } } + /** + * Exports a table's data with search and replace. + * + * @param string $table The table to export. + * @param string $old The string to search for. + * @param string $new The string to replace with. + * @return array A tuple containing the report array and the total number of rows. + */ private function php_export_table( $table, $old, $new ) { list( $primary_keys, $columns, $all_columns ) = self::get_columns( $table ); @@ -675,6 +683,17 @@ private function php_export_table( $table, $old, $new ) { return array( $table_report, $total_rows ); } + /** + * Performs a search and replace on a column using SQL. + * + * @param string $col The column to perform the search-replace on. + * @param string[] $primary_keys The primary keys of the table. + * @param string $table The table to perform the search-replace on. + * @param string $old The string to search for. + * @param string $new The string to replace with. + * @param string[] $clauses The WHERE clauses to apply. + * @return int The number of replacements made. + */ private function sql_handle_col( $col, $primary_keys, $table, $old, $new, $clauses ) { global $wpdb; @@ -704,6 +723,17 @@ private function sql_handle_col( $col, $primary_keys, $table, $old, $new, $claus return $count; } + /** + * Performs a search and replace on a column using PHP. + * + * @param string $col The column to perform the search-replace on. + * @param string[] $primary_keys The primary keys of the table. + * @param string $table The table to perform the search-replace on. + * @param string $old The string to search for. + * @param string $new The string to replace with. + * @param string[] $additional_where The additional WHERE clauses to apply. + * @return int The number of replacements made. + */ private function php_handle_col( $col, $primary_keys, $table, $old, $new, $additional_where ) { global $wpdb; @@ -818,6 +848,12 @@ static function ( $key ) { return $count; } + /** + * Writes a set of rows to the export file as SQL INSERT statements. + * + * @param string $table The table to write the rows for. + * @param array $rows The rows to write. + */ private function write_sql_row_fields( $table, $rows ) { global $wpdb; @@ -888,6 +924,12 @@ private function write_sql_row_fields( $table, $rows ) { } } + /** + * Gets the primary keys, text columns, and all columns for a table. + * + * @param string $table The table to get the columns for. + * @return array A tuple containing the primary keys, text columns, and all columns. + */ private static function get_columns( $table ) { global $wpdb; @@ -915,6 +957,12 @@ private static function get_columns( $table ) { return array( $primary_keys, $text_columns, $all_columns ); } + /** + * Checks if a column type is a text type. + * + * @param string $type The column type to check. + * @return bool Whether the column type is a text type. + */ private static function is_text_col( $type ) { foreach ( array( 'text', 'varchar' ) as $token ) { if ( false !== stripos( $type, $token ) ) { @@ -925,6 +973,12 @@ private static function is_text_col( $type ) { return false; } + /** + * Escapes a string for a MySQL LIKE statement. + * + * @param string $old The string to escape. + * @return string The escaped string. + */ private static function esc_like( $old ) { global $wpdb; @@ -1020,14 +1074,15 @@ private function get_colors( $assoc_args, $colors ) { return $colors; } - /* + /** * Logs the difference between old match and new replacement for SQL replacement. * - * @param string $col Column being processed. - * @param array $primary_keys Primary keys for table. - * @param string $table Table being processed. - * @param string $old Old value to match. - * @param string $new New value to replace the old value with. + * @param string $col Column being processed. + * @param string[] $primary_keys Primary keys for table. + * @param string $table Table being processed. + * @param string $old Old value to match. + * @param string $new New value to replace the old value with. + * @param string[] $clauses The WHERE clauses to apply. * @return int Count of changed rows. */ private function log_sql_diff( $col, $primary_keys, $table, $old, $new, $clauses ) { @@ -1062,15 +1117,15 @@ private function log_sql_diff( $col, $primary_keys, $table, $old, $new, $clauses return count( $results ); } - /* + /** * Logs the difference between old matches and new replacements at the end of a PHP (regex) replacement of a database row. * - * @param string $col Column being processed. - * @param array $keys Associative array (or object) of primary key names and their values for the row being processed. - * @param string $table Table being processed. - * @param string $old Old value to match. - * @param string $new New value to replace the old value with. - * @param array $log_data Array of data strings before replacements. + * @param string $col Column being processed. + * @param array|object $keys Associative array (or object) of primary key names and their values for the row being processed. + * @param string $table Table being processed. + * @param string $old Old value to match. + * @param string $new New value to replace the old value with. + * @param string[] $log_data Array of data strings before replacements. */ private function log_php_diff( $col, $keys, $table, $old, $new, $log_data ) { if ( $this->regex ) { @@ -1196,14 +1251,14 @@ static function ( $m ) use ( $matches ) { return array( $old_bits, $new_bits ); } - /* + /** * Outputs the log strings. * - * @param string $col Column being processed. - * @param array $keys Associative array (or object) of primary key names and their values for the row being processed. - * @param string $table Table being processed. - * @param array $old_bits Array of old match log strings. - * @param array $new_bits Array of new replacement log strings. + * @param string $col Column being processed. + * @param array|object $keys Associative array (or object) of primary key names and their values for the row being processed. + * @param string $table Table being processed. + * @param string[] $old_bits Array of old match log strings. + * @param string[] $new_bits Array of new replacement log strings. */ private function log_write( $col, $keys, $table, $old_bits, $new_bits ) { if ( ! $this->log_handle ) { @@ -1226,9 +1281,15 @@ private function log_write( $col, $keys, $table, $old_bits, $new_bits ) { fwrite( $this->log_handle, "{$table_column_id_log}\n{$old_log}\n{$new_log}\n" ); } + /** + * Develops the WHERE specifications from a string. + * + * @param string|null $str_specs The string of WHERE specifications. + * @return array The WHERE specifications as an array. + */ private function develop_where_specs( $str_specs ) { global $wpdb; - $specs = array_filter( explode( ';', $str_specs ) ); + $specs = array_filter( explode( ';', (string) $str_specs ) ); $clauses = []; foreach ( $specs as $spec ) { list( $tables, $cols, $conditions ) = explode( ':', $spec, 3 ); @@ -1244,12 +1305,22 @@ private function develop_where_specs( $str_specs ) { return $clauses; } + /** + * Excludes revisions from the search-replace operation. + */ private function no_revision() { global $wpdb; $this->where[ $wpdb->posts ]['*'][] = self::esc_sql_ident( 'post_status' ) . '=' . self::esc_sql_value( 'publish' ); $this->where[ $wpdb->postmeta ]['*'][] = self::esc_sql_ident( 'post_id' ) . ' IN ( SELECT ID FROM ' . $wpdb->posts . ' WHERE ' . self::esc_sql_ident( 'post_status' ) . '=' . self::esc_sql_value( 'publish' ) . ')'; } + /** + * Gets the WHERE clauses for a given table and column. + * + * @param string $table The table to get the clauses for. + * @param string|null $column The column to get the clauses for. + * @return string[] The WHERE clauses. + */ private function get_clauses( $table, $column = null ) { return array_filter( array_merge( diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index 80e9b5af..204f256f 100644 --- a/src/WP_CLI/SearchReplacer.php +++ b/src/WP_CLI/SearchReplacer.php @@ -73,6 +73,7 @@ class SearchReplacer { * @param string $regex_delimiter Delimiter for regular expression. * @param bool $logging Whether logging. * @param integer $regex_limit The maximum possible replacements for each pattern in each subject string. + * @param callable|string|false $callback The callback function to invoke. */ public function __construct( $from, $to, $recurse_objects = false, $regex = false, $regex_flags = '', $regex_delimiter = '/', $logging = false, $regex_limit = -1, $callback = false ) { $this->from = $from; @@ -97,16 +98,23 @@ public function __construct( $from, $to, $recurse_objects = false, $regex = fals * * @param array|string $data The data to operate on. * @param bool $serialised Does the value of $data need to be unserialized? + * @param array $opts Options for the callback. * - * @return array The original array with all elements replaced as needed. + * @return array|string The original array with all elements replaced as needed. */ public function run( $data, $serialised = false, $opts = [] ) { return $this->run_recursively( $data, $serialised, 0, [], $opts ); } /** - * @param int $recursion_level Current recursion depth within the original data. - * @param array $visited_data Data that has been seen in previous recursion iterations. + * The main workhorse of the run method. + * + * @param mixed $data The data to operate on. + * @param bool $serialised Does the value of $data need to be unserialized? + * @param int $recursion_level Current recursion depth within the original data. + * @param array $visited_data Data that has been seen in previous recursion iterations. + * @param array $opts Options for the callback. + * @return mixed The original data with all elements replaced as needed. */ private function run_recursively( $data, $serialised, $recursion_level = 0, $visited_data = array(), $opts = [] ) { @@ -269,7 +277,7 @@ public function clear_log_data() { /** * Get the PCRE error constant name from an error value. * - * @param integer $error Error code. + * @param int|null $error Error code. * @return string Error constant name. */ private function preg_error_message( $error ) { From c5e06bdde7b5b204d085d09913dd38b249a4a5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Tue, 11 Nov 2025 21:20:35 -0300 Subject: [PATCH 14/19] various fixes --- features/search-replace-callback.feature | 60 +++++++++++++++++++++++- src/Search_Replace_Command.php | 11 +++-- src/WP_CLI/SearchReplacer.php | 2 + 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/features/search-replace-callback.feature b/features/search-replace-callback.feature index cc1df240..373f37fb 100644 --- a/features/search-replace-callback.feature +++ b/features/search-replace-callback.feature @@ -6,7 +6,11 @@ Feature: Test search-replace --callback option And a callback-function.php file: """ >|false + * @var array> */ - private $where; + private $where = []; /** * @var bool @@ -219,7 +219,12 @@ class Search_Replace_Command extends WP_CLI_Command { * : Prints rows to the console as they're updated. * * [--callback=] - * : Runs a user-specified function on each string that contains . is passed as the second argument and the regex string as the third if it exists: call_user_func( 'callback', $data, $new, $search_regex ). + * : Runs a user-specified function on each string that contains . The callback is called as follows: + * call_user_func( 'callback', $data, $new, $search_regex, $opts ) + * * $data is the matched string + * * $new is the replacement string + * * $search_regex is the regex pattern (if applicable, null otherwise) + * * $opts is an array of options. * * [--regex] * : Runs the search using a regular expression (without delimiters). diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index 204f256f..6fe80a66 100644 --- a/src/WP_CLI/SearchReplacer.php +++ b/src/WP_CLI/SearchReplacer.php @@ -97,6 +97,8 @@ public function __construct( $from, $to, $recurse_objects = false, $regex = fals * Ignores any serialized objects unless $recurse_objects is set to true. * * @param array|string $data The data to operate on. + * @param array $opts Additional options for the replacement passed through to the callback + * An array like: ['table' => $table, 'col' => $col, 'key' => $primary_keys] * @param bool $serialised Does the value of $data need to be unserialized? * @param array $opts Options for the callback. * From 363b893ce8270251f6afeacd17ab85887a8c546f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Wed, 12 Nov 2025 00:12:00 -0300 Subject: [PATCH 15/19] misc fixes --- features/search-replace-where.feature | 2 +- src/Search_Replace_Command.php | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/features/search-replace-where.feature b/features/search-replace-where.feature index 5ed528c0..0c71d3b1 100644 --- a/features/search-replace-where.feature +++ b/features/search-replace-where.feature @@ -217,7 +217,7 @@ Feature: Test search-replace --where option When I try `wp search-replace 'foobar' 'replaced' --where='posts::post_status="publish"' --include-columns=post_title` Then STDERR should contain: """ - Warning: Column-catch was passed to --where while. But --include-columns will still restrict replacements to columns: post_title + Warning: Column wildcard (*) was passed to --where. But --include-columns will still restrict replacements to columns: post_title """ And STDOUT should contain: diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index a2fa68a8..eeb9fe84 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -483,7 +483,7 @@ function ( $e ) { WP_CLI::warning( 'Column wildcard (*) was passed to --where. But --include-columns will still restrict replacements to columns: ' . implode( ',', $this->include_columns ) ); } } - $this->include_columns = array_merge( $this->include_columns, $columns ); + $this->include_columns = array_merge( $this->include_columns, $columns ); } } @@ -1297,9 +1297,11 @@ private function develop_where_specs( $str_specs ) { $specs = array_filter( explode( ';', (string) $str_specs ) ); $clauses = []; foreach ( $specs as $spec ) { - list( $tables, $cols, $conditions ) = explode( ':', $spec, 3 ); - $tables = array_filter( explode( ',', $tables ) ); - $cols = array_filter( explode( ',', $cols ) ) ? : [ '*' ]; + $parts = array_map( 'trim', explode( ':', $spec, 3 ) ); + if ( count( $parts ) < 3 ) continue; + list( $tables, $cols, $conditions ) = $parts; + $tables = array_filter( array_map( 'trim', explode( ',', $tables ) ) ); + $cols = array_filter( array_map( 'trim', explode( ',', $cols ) ) ) ?: ['*']; foreach ( $tables as $table ) { foreach ( $cols as $col ) { $clauses[ empty( $wpdb->{$table} ) ? $table : $wpdb->{$table} ][ $col ][] = $conditions; From a821c3dfbea5a4e34836939ce725c0bc53c5f4f2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 12 Nov 2025 09:00:25 +0100 Subject: [PATCH 16/19] Lint fixes --- src/Search_Replace_Command.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index eeb9fe84..a3e5ba88 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -1298,10 +1298,12 @@ private function develop_where_specs( $str_specs ) { $clauses = []; foreach ( $specs as $spec ) { $parts = array_map( 'trim', explode( ':', $spec, 3 ) ); - if ( count( $parts ) < 3 ) continue; + if ( count( $parts ) < 3 ) { + continue; + } list( $tables, $cols, $conditions ) = $parts; - $tables = array_filter( array_map( 'trim', explode( ',', $tables ) ) ); - $cols = array_filter( array_map( 'trim', explode( ',', $cols ) ) ) ?: ['*']; + $tables = array_filter( array_map( 'trim', explode( ',', $tables ) ) ); + $cols = array_filter( array_map( 'trim', explode( ',', $cols ) ) ) ?: [ '*' ]; foreach ( $tables as $table ) { foreach ( $cols as $col ) { $clauses[ empty( $wpdb->{$table} ) ? $table : $wpdb->{$table} ][ $col ][] = $conditions; From 001381a964bfcc66888f387307dfc107239ece92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Wed, 12 Nov 2025 11:47:25 -0300 Subject: [PATCH 17/19] Update src/WP_CLI/SearchReplacer.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/WP_CLI/SearchReplacer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index 6fe80a66..6c09c0f2 100644 --- a/src/WP_CLI/SearchReplacer.php +++ b/src/WP_CLI/SearchReplacer.php @@ -236,7 +236,7 @@ private function run_recursively( $data, $serialised, $recursion_level = 0, $vis if ( $this->callback ) { if ( false === $result ) { - \WP_CLI::error( 'The callback function return false. Stopping operation.' ); + \WP_CLI::error( 'The callback function returned false. Stopping operation.' ); } elseif ( is_wp_error( $result ) ) { $message = $result->get_error_message(); \WP_CLI::error( 'The callback function threw an error. Stopping operation. ' . $message ); From 5f8498c51a358c398bc1dfbdc4c59dc15247c24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Wed, 12 Nov 2025 11:49:18 -0300 Subject: [PATCH 18/19] Update src/Search_Replace_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Search_Replace_Command.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index a3e5ba88..791f1006 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -204,8 +204,7 @@ class Search_Replace_Command extends WP_CLI_Command { * format:
[,table]:[column,...]:quoted-SQL-condition * * [--revisions] - * : Default true. - * If false, then identical to --where='posts::post_status="publish";postmeta::post_id IN (SELECT ID FROM {posts} WHERE post_status="publish")' + * : Include revisions in the search/replace. Defaults to true; pass --no-revisions to exclude revisions (identical to --where='posts::post_status="publish";postmeta::post_id IN (SELECT ID FROM {posts} WHERE post_status="publish")'). * * [--precise] * : Force the use of PHP (instead of SQL) which is more thorough, From 816a51abbad4e8caf11b264fb2a54d87e884f284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Wed, 12 Nov 2025 11:50:04 -0300 Subject: [PATCH 19/19] Update src/Search_Replace_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Search_Replace_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index 791f1006..85561cf4 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -131,7 +131,7 @@ class Search_Replace_Command extends WP_CLI_Command { private $where = []; /** - * @var bool + * @var string|false */ private $callback;