Skip to content
144 changes: 103 additions & 41 deletions src/Theme_Mod_Command.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

use \WP_CLI\Utils;

Check failure on line 3 in src/Theme_Mod_Command.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

An import use statement should never start with a leading backslash
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The import here uses a leading backslash, whereas other commands in this repository consistently use use WP_CLI\Utils; without it (for example, src/Theme_Command.php:5 and src/Plugin_Command.php:5). For consistency with established codebase conventions, it would be better to drop the leading backslash from this use statement.

Copilot uses AI. Check for mistakes.

/**
* Sets, gets, and removes theme mods.
*
Expand Down Expand Up @@ -80,57 +82,117 @@
*/
public function get( $args, $assoc_args ) {

if ( ! \WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ) && empty( $args ) ) {
if ( ! Utils\get_flag_value( $assoc_args, 'all' ) && empty( $args ) ) {
WP_CLI::error( 'You must specify at least one mod or use --all.' );
}

if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ) ) {
if ( Utils\get_flag_value( $assoc_args, 'all' ) ) {
$args = array();
}

$list = array();
$mods = get_theme_mods();
if ( ! is_array( $mods ) ) {
// If no mods are set (perhaps new theme), make sure foreach still works.
$mods = array();
}
foreach ( $mods as $k => $v ) {
// If mods were given, skip the others.
if ( ! empty( $args ) && ! in_array( $k, $args, true ) ) {
continue;
}
// This array will hold the list of theme mods in a format suitable for the WP CLI Formatter.
$mod_list = array();

if ( is_array( $v ) ) {
$list[] = [
'key' => $k,
'value' => '=>',
];
foreach ( $v as $_k => $_v ) {
$list[] = [
'key' => " $_k",
'value' => $_v,
];
}
} else {
$list[] = [
'key' => $k,
'value' => $v,
];
}
}
// If specific mods are requested, filter out any that aren't requested.
$mods = ! empty( $args ) ? array_intersect_key( get_theme_mods(), array_flip( $args ) ) : get_theme_mods();

// For unset mods, show blank value.
foreach ( $args as $mod ) {
if ( ! isset( $mods[ $mod ] ) ) {
$list[] = [
'key' => $mod,
'value' => '',
];
// Generate the list of items ready for output. We use an initial separator that we can replace later depending on format.
$separator = '\t';
array_walk(
$mods,
function ( $value, $key ) use ( &$mod_list, $separator ) {
Comment on lines +96 to +103
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The previous implementation added rows for requested mods that are not present in get_theme_mods() (see the old "For unset mods, show blank value." logic), which is relied on by the theme-mod.feature example for header_textcolor. With this new $mods construction and no subsequent pass over $args, requested-but-unset mods are now silently omitted from the output for all formats, changing the CLI behavior; to preserve backward compatibility you should add entries with an empty value for any requested mod that is not present in $mods (at least for the table format, and optionally for others).

Copilot uses AI. Check for mistakes.
$this->mod_to_string( $key, $value, $mod_list, $separator );
}
);

// Take our Formatter-friendly list and adjust it according to the requested format.
switch ( Utils\get_flag_value( $assoc_args, 'format' ) ) {
// For tables we use a double space to indent child items.
case 'table':
$mod_list = array_map(
static function ( $item ) use ( $separator ) {
$parts = explode( $separator, $item['key'] );
$new_key = array_pop( $parts );
if ( ! empty( $parts ) ) {
$new_key = str_repeat( ' ', count( $parts ) ) . $new_key;
}
return [
'key' => $new_key,
'value' => $item['value'],
];
},
$mod_list
);
break;

// For JSON, CSV, and YAML formats we use dot notation to show the hierarchy.
case 'csv':
case 'yaml':
case 'json':
$mod_list = array_filter(
array_map(
static function ( $item ) use ( $separator ) {
return [
'key' => str_replace( $separator, '.', $item['key'] ),
'value' => $item['value'],
];
},
$mod_list
),
function ( $item ) {
return ! empty( $item['value'] );
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

Using empty( $item['value'] ) here will also filter out legitimate values like '0' or 0 from the CSV/YAML/JSON output, not just missing values. If the intent is to strip only items with no value, consider checking explicitly for null or an empty string instead of relying on empty(), so that falsy-but-meaningful values are still emitted.

Suggested change
return ! empty( $item['value'] );
return $item['value'] !== '' && $item['value'] !== null;

Copilot uses AI. Check for mistakes.
}
);
break;
Comment on lines +128 to +146
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The new flattening and dot-notation logic for CSV/YAML/JSON formats introduces non-trivial behavior but currently has no dedicated acceptance tests (unlike features/theme-mod-list.feature, which covers those formats for the list subcommand). Given that issue #96 was caused by malformed non-tabular output, it would be valuable to add Behat coverage for wp theme mod get --all --format=csv|json|yaml with nested theme-mod structures to ensure this formatter behavior is exercised and guarded against regressions.

Copilot uses AI. Check for mistakes.
}

// Output the list using the WP CLI Formatter.
$formatter = new \WP_CLI\Formatter( $assoc_args, $this->fields, 'thememods' );
$formatter->display_items( $list );
$formatter->display_items( $mod_list );
}

/**
* Convert the theme mods to a flattened array with a string representation of the keys.
*
* @param string $key The mod key
* @param mixed $value The value of the mod.
* @param array $mod_list The list so far, passed by reference.
* @param string $separator A string to separate keys to denote their place in the tree.
*/
private function mod_to_string( $key, $value, &$mod_list, $separator ) {
if ( is_array( $value ) || is_object( $value ) ) {
// Convert objects to arrays for easier handling.
$value = (array) $value;

// Explicitly handle empty arrays to ensure they are displayed.
if ( empty( $value ) ) {
$mod_list[] = array(
'key' => $key,
'value' => '[empty array]',
);
return;
}

// Arrays get their own entry in the list to allow for sensible table output.
$mod_list[] = array(
'key' => $key,
'value' => '',
);

foreach ( $value as $child_key => $child_value ) {
$this->mod_to_string( $key . $separator . $child_key, $child_value, $mod_list, $separator );
}
} else {
// Explicitly handle boolean values to ensure they are displayed correctly.
if ( is_bool( $value ) ) {
$value = $value ? '[true]' : '[false]';
}

$mod_list[] = array(
'key' => $key,
'value' => $value,
);
}
}

/**
Expand Down Expand Up @@ -206,11 +268,11 @@
*/
public function remove( $args, $assoc_args ) {

if ( ! \WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ) && empty( $args ) ) {
if ( ! Utils\get_flag_value( $assoc_args, 'all' ) && empty( $args ) ) {
WP_CLI::error( 'You must specify at least one mod or use --all.' );
}

if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ) ) {
if ( Utils\get_flag_value( $assoc_args, 'all' ) ) {
remove_theme_mods();
WP_CLI::success( 'Theme mods removed.' );
return;
Expand Down
Loading