Skip to content

feat: add flag --replace-keys to handle replacing string array key values#236

Open
AlextheYounga wants to merge 3 commits intowp-cli:mainfrom
AlextheYounga:fix/alexy/replace-keys
Open

feat: add flag --replace-keys to handle replacing string array key values#236
AlextheYounga wants to merge 3 commits intowp-cli:mainfrom
AlextheYounga:fix/alexy/replace-keys

Conversation

@AlextheYounga
Copy link
Copy Markdown

Attempts to address #137 by adding an opt-in --replace-keys flag so string keys in serialized arrays can be replaced when needed.

wp search-replace currently traverses serialized values but skips array keys, which prevents some real-world migrations (e.g. sidebar keys, URL-keyed plugin options) from being updated. By default, existing behavior is unchanged: array keys are still ignored unless --replace-keys is explicitly provided.

What changed

  • Added new command option:
    • --replace-keys: enables replacing string keys in serialized arrays.
  • Wired the flag through command execution into SearchReplacer.
  • Updated recursive replacement logic to:
    • replace array values as before
    • optionally replace string keys when --replace-keys is enabled
  • Added acceptance coverage in Behat for:
    • no-flag baseline (keys remain unchanged)
    • basic key replacement
    • nested key replacement
    • key + value replacement
    • key-collision behavior
    • regex + replace-keys behavior

Testing

Ran targeted Behat scenarios for replace-keys behavior and edge cases:

  • Search and replace serialized array keys with --replace-keys
  • Search and replace nested serialized array keys with --replace-keys
  • Search and replace key and value with --replace-keys
  • Search and replace colliding serialized array keys with --replace-keys

🫡

This change attempts to correct issue wp-cli#137 by replacing keys
for string arrays with a new --replace-keys flag. Existing
default behavior remains unchanged when the flag is absent.

Refs:
  wp-cli#137
@AlextheYounga AlextheYounga requested a review from a team as a code owner May 10, 2026 01:22
@github-actions github-actions Bot added command:search-replace Related to 'search-replace' command scope:testing Related to testing labels May 10, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new --replace-keys flag to the search-replace command, enabling string replacement within serialized array keys. The changes include updates to the command logic, the SearchReplacer class, and new Behat test scenarios. Feedback highlights a potential issue with in-place array modification during iteration which could cause data loss, and a breaking change in the SearchReplacer constructor signature that affects backward compatibility.

Comment on lines +175 to 186
$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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Modifying the array in-place while iterating over its keys can lead to incorrect results and data loss. If a key is replaced with a name that exists later in the original array, the value associated with that new key will be overwritten before it is processed. Furthermore, the loop will eventually reach that new key and process the already-replaced value a second time.

To fix this, consider building a new array for the transformed data instead of modifying $data in-place. This ensures each original key-value pair is processed exactly once and correctly handles scenarios like key swaps.

Example fix:

$new_data = [];
foreach ( $data as $key => $value ) {
    $new_value = $this->run_recursively( $value, false, $recursion_level + 1, $visited_data );
    $new_key   = $key;
    if ( $this->replace_keys && is_string( $key ) ) {
        $new_key = $this->run_recursively( $key, false, $recursion_level + 1, $visited_data );
    }
    $new_data[ $new_key ] = $new_value;
}
$data = $new_data;

* @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 ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The addition of the $replace_keys parameter in the middle of the constructor's argument list is a breaking change for the SearchReplacer class's public API. Any existing code calling this constructor with positional arguments (e.g., to set $regex or $logging) will now pass those values to the wrong parameters. It is recommended to append new parameters to the end of the argument list to maintain backward compatibility.

public function __construct( $from, $to, $recurse_objects = false, $regex = false, $regex_flags = '', $regex_delimiter = '/', $logging = false, $regex_limit = -1, $replace_keys = false ) {

);

$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 );
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If the SearchReplacer constructor signature is updated to append $replace_keys at the end (to maintain backward compatibility), this call should be updated accordingly.

$replacer   = new SearchReplacer( $old, $new, $this->recurse_objects, $this->regex, $this->regex_flags, $this->regex_delimiter, false, $this->regex_limit, $this->replace_keys );


$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 );
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If the SearchReplacer constructor signature is updated to append $replace_keys at the end (to maintain backward compatibility), this call should be updated accordingly.

$replacer = new SearchReplacer( $old, $new, $this->recurse_objects, $this->regex, $this->regex_flags, $this->regex_delimiter, null !== $this->log_handle, $this->regex_limit, $this->replace_keys );

@codecov
Copy link
Copy Markdown

codecov Bot commented May 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

command:search-replace Related to 'search-replace' command scope:testing Related to testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant