diff --git a/features/search-replace.feature b/features/search-replace.feature index 2bccd606..6fdd653f 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -303,17 +303,102 @@ Feature: Do global search/replace http:\/\/example.com """ - When I run `wp search-replace 'http://newdomain.com' 'http://example.com' wp_posts --include-columns=post_content --precise` + Scenario: Search and replace serialized array keys with --replace-keys + Given a WP install + And a setup-serialized-array-key.php file: + """ + 'value' ) ); + """ + And I run `wp eval-file setup-serialized-array-key.php` + + When I run `wp search-replace sr-key-old sr-key-new --dry-run` Then STDOUT should be a table containing rows: - | Table | Column | Replacements | Type | - | wp_posts | post_content | 1 | PHP | + | Table | Column | Replacements | Type | + | wp_options | option_value | 0 | PHP | - When I run `wp post get {POST_ID} --field=post_content` - Then STDOUT should contain: + When I run `wp search-replace sr-key-old sr-key-new --replace-keys --dry-run` + Then STDOUT should be a table containing rows: + | Table | Column | Replacements | Type | + | wp_options | option_value | 1 | PHP | + + When I run `wp search-replace sr-key-old sr-key-new --replace-keys` + And I run `wp option get replace_key_option --format=json` + Then STDOUT should be JSON containing: """ - http:\/\/example.com + {"sr-key-new":"value"} """ + Scenario: Search and replace nested serialized array keys with --replace-keys + Given a WP install + And a setup-nested-serialized-array-key.php file: + """ + array( + 'sr-key-old' => 'value', + ), + ) + ); + """ + And I run `wp eval-file setup-nested-serialized-array-key.php` + + When I run `wp search-replace sr-key-old sr-key-new --replace-keys --dry-run` + Then STDOUT should be a table containing rows: + | Table | Column | Replacements | Type | + | wp_options | option_value | 1 | PHP | + + When I run `wp search-replace sr-key-old sr-key-new --replace-keys` + And I run `wp option get replace_nested_key_option --format=json` + Then STDOUT should be JSON containing: + """ + {"outer":{"sr-key-new":"value"}} + """ + + Scenario: Search and replace key and value with --replace-keys + Given a WP install + And a setup-key-and-value-option.php file: + """ + 'sr-key-old' ) ); + """ + And I run `wp eval-file setup-key-and-value-option.php` + + When I run `wp search-replace sr-key-old sr-key-new --replace-keys --dry-run` + Then STDOUT should be a table containing rows: + | Table | Column | Replacements | Type | + | wp_options | option_value | 1 | PHP | + + When I run `wp search-replace sr-key-old sr-key-new --replace-keys` + And I run `wp option get replace_key_and_value_option --format=json` + Then STDOUT should be JSON containing: + """ + {"sr-key-new":"sr-key-new"} + """ + + Scenario: Search and replace colliding serialized array keys with --replace-keys + Given a WP install + And a setup-colliding-keys-option.php file: + """ + 'from-old', + 'sr-key-new' => 'existing', + ) + ); + """ + And I run `wp eval-file setup-colliding-keys-option.php` + + When I run `wp search-replace sr-key-old sr-key-new --replace-keys` + And I run `wp option get replace_colliding_keys_option --format=json` + Then STDOUT should be JSON containing: + """ + {"sr-key-new":"from-old"} + """ @require-mysql Scenario: Search and replace with quoted strings Given a WP install diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index 8423560d..88445ecf 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -37,6 +37,11 @@ class Search_Replace_Command extends WP_CLI_Command { */ private $recurse_objects; + /** + * @var bool + */ + private $replace_keys; + /** * @var bool */ @@ -214,6 +219,9 @@ class Search_Replace_Command extends WP_CLI_Command { * : Enable recursing into objects to replace strings. Defaults to true; * pass --no-recurse-objects to disable. * + * [--replace-keys] + * : Enable replacing string keys in serialized arrays. + * * [--verbose] * : Prints rows to the console as they're updated. * @@ -287,7 +295,7 @@ class Search_Replace_Command extends WP_CLI_Command { * fi * * @param array $args Positional arguments. - * @param array{'old'?: string, 'new'?: string, 'dry-run'?: bool, 'network'?: bool, 'all-tables-with-prefix'?: bool, 'all-tables'?: bool, 'export'?: string, 'export_insert_size'?: string, 'skip-tables'?: string, 'skip-columns'?: string, 'include-columns'?: string, 'precise'?: bool, 'recurse-objects'?: bool, 'verbose'?: bool, 'regex'?: bool, 'regex-flags'?: string, 'regex-delimiter'?: string, 'regex-limit'?: string, 'format': string, 'report'?: bool, 'report-changed-only'?: bool, 'log'?: string, 'before_context'?: string, 'after_context'?: string} $assoc_args Associative arguments. + * @param array{'old'?: string, 'new'?: string, 'dry-run'?: bool, 'network'?: bool, 'all-tables-with-prefix'?: bool, 'all-tables'?: bool, 'export'?: string, 'export_insert_size'?: string, 'skip-tables'?: string, 'skip-columns'?: string, 'include-columns'?: string, 'precise'?: bool, 'recurse-objects'?: bool, 'replace-keys'?: bool, 'verbose'?: bool, 'regex'?: bool, 'regex-flags'?: string, 'regex-delimiter'?: string, 'regex-limit'?: string, 'format': string, 'report'?: bool, 'report-changed-only'?: bool, 'log'?: string, 'before_context'?: string, 'after_context'?: string} $assoc_args Associative arguments. */ public function __invoke( $args, $assoc_args ) { global $wpdb; @@ -333,6 +341,7 @@ 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->replace_keys = Utils\get_flag_value( $assoc_args, 'replace-keys', 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 ); @@ -638,7 +647,7 @@ private function php_export_table( $table, $old, $new ) { 'chunk_size' => $chunk_size, ); - $replacer = new SearchReplacer( $old, $new, $this->recurse_objects, $this->regex, $this->regex_flags, $this->regex_delimiter, false, $this->regex_limit ); + $replacer = new SearchReplacer( $old, $new, $this->recurse_objects, $this->replace_keys, $this->regex, $this->regex_flags, $this->regex_delimiter, false, $this->regex_limit ); $col_counts = array_fill_keys( $all_columns, 0 ); if ( $this->verbose && 'table' === $this->format ) { $this->start_time = microtime( true ); @@ -743,7 +752,7 @@ private function php_handle_col( $col, $primary_keys, $table, $old, $new ) { 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->replace_keys, $this->regex, $this->regex_flags, $this->regex_delimiter, null !== $this->log_handle, $this->regex_limit ); $table_sql = self::esc_sql_ident( $table ); $col_sql = self::esc_sql_ident( $col ); diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index 6f3bea9f..5e00946b 100644 --- a/src/WP_CLI/SearchReplacer.php +++ b/src/WP_CLI/SearchReplacer.php @@ -32,6 +32,11 @@ class SearchReplacer { */ private $recurse_objects; + /** + * @var bool + */ + private $replace_keys; + /** * @var bool */ @@ -71,16 +76,18 @@ class SearchReplacer { * @param string $from String we're looking to replace. * @param string $to What we want it to be replaced with. * @param bool $recurse_objects Should objects be recursively replaced? + * @param bool $replace_keys Should array keys be replaced? * @param bool $regex Whether `$from` is a regular expression. * @param string $regex_flags Flags for regular expression. * @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. */ - 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, $replace_keys = false, $regex = false, $regex_flags = '', $regex_delimiter = '/', $logging = false, $regex_limit = -1 ) { $this->from = $from; $this->to = $to; $this->recurse_objects = $recurse_objects; + $this->replace_keys = $replace_keys; $this->regex = $regex; $this->regex_flags = $regex_flags; $this->regex_delimiter = $regex_delimiter; @@ -165,7 +172,17 @@ private function run_recursively( $data, $serialised, $recursion_level = 0, $vis } elseif ( is_array( $data ) ) { $keys = array_keys( $data ); foreach ( $keys as $key ) { - $data[ $key ] = $this->run_recursively( $data[ $key ], false, $recursion_level + 1, $visited_data ); + $value = $this->run_recursively( $data[ $key ], false, $recursion_level + 1, $visited_data ); + if ( $this->replace_keys && is_string( $key ) ) { + $replaced_key = $this->run_recursively( $key, false, $recursion_level + 1, $visited_data ); + if ( $replaced_key !== $key ) { + unset( $data[ $key ] ); + $data[ $replaced_key ] = $value; + continue; + } + } + + $data[ $key ] = $value; } } elseif ( $this->recurse_objects && ( is_object( $data ) || $data instanceof \__PHP_Incomplete_Class ) ) { if ( $data instanceof \__PHP_Incomplete_Class ) {