Skip to content

Extending an existing command stopped working in 2.2.0 #5274

@pbiron

Description

@pbiron

Bug Report

In WP_CLI 2.1.0, I could "extend" an existing command (whether builtin or provided by some other plugin/package). For example,

class My_Extended_Plugin_Command extends Plugin_Command {
	function install( $args, $assoc_args ) {
		if ( ! issset( $assoc_args['my-flag'] ) ) {
			parent::install( $args, $assoc_args );
			
			exit;
		}
		
		// do something with the my-flag arg that isn't
		// part of the built-in plugin command.
		...
	}
}

WP_CLI::add_command( 'plugin', 'My_Extended_Plugin_Command' );

However, with the release of WP_CLI 2.2.0, this no longer works.

Instead, when I run wp plugin install [some-plugin] --my-flag, WP_CLI outputs:

Error: Parameter errors:
 unknown --my-flag parameter

Looking at the output of wp plugin install [some-plugin] --my-flag --debug (whether running 2.1.0 or 2.2.0), I see:

...
Debug (commands): Adding command: plugin (0.198s)
...
Debug (commands): Adding command: plugin in language Namespace (0.319s)
...
Debug (commands): Adding command: plugin (1.27s)
...
Debug (bootstrap): Running command: plugin install (2.865s)

with no errors saying that my WP_CLI::add_command( 'plugin', 'My_Extended_Plugin_Command' ) failed when running 2.2.0+.

The release notes for 2.2.0 mention a few breaking changes, including a note that:

This is mostly relevant if you extend one of the WP_CLI classes to override default behavior.

But that note is in a section about Name Changes to "...internal functions, methods and properties..." and the change in behavior between 2.1.0 and 2.2.0 when extending an existing command class seems to have more to do with "bootstrapping" that takes place in WP_CLI::add_command().

A while ago I asked in Slack (here and here about this change in behavior but got no reply either time.

Describe what you expect as the correct outcome

In 2.1.0, the call to WP_CLI::add_command( 'plugin', 'My_Extended_Plugin_Command' ) in my plugin results in the My_Extended_Plugin_Command class being used for the plguin command instead of the global Plugin_Command class. In 2.2.0+, the global Plugin_Command class is used. I expect the My_Extended_Plugin_Command class to be used regardless of which version of WP_CLI I'm using.

Let us know what environment you are running this on

OS:     Windows NT 10.0 build 18362 (Windows 10) AMD64
Shell:  C:\WINDOWS\system32\cmd.exe
PHP binary:     C:\Program Files\lang\php\7.2.22\php.exe
PHP version:    7.2.22
php.ini used:   C:\Program Files\lang\php\7.2.22\php.ini
WP_CLI root dir:        phar://WP_CLI.phar/vendor/WP_CLI/WP_CLI
WP_CLI vendor dir:      phar://WP_CLI.phar/vendor
WP_CLI phar path:       C:\Users\pbiron\Documents\htdocs\shc\code-ref
WP_CLI packages dir:    C:\Users\pbiron\.WP_CLI\packages/
WP_CLI global config:   c:\Users\pbiron\.WP_CLI\config.yml
WP_CLI project config:  C:\Users\pbiron\Documents\htdocs\shc\code-ref\WP_CLI.yml
WP_CLI version: 2.3.0

Use Cases

Why would one want to extend an existing command? Here are a few use cases where I have done so.

shared plugins: extending builtin plugin command

On my local dev machine, I store ALL plugins in a centralized location (outside of any particular WP installation) and symlink them into wp-content/plugins of the various sites I have running. I do this so that, among other reasons, when a new version of a plugin is released I only have to update it once and all my local sites that use it automatically get that updated version.

ETA: Actually, I don't store all of my plugins in the shared/centralized location. Some times I install a plugin just on one site...until I'm fairly confident in the code quality of the plugin and that updates to it aren't likely to break a site...and then I'll turn it into a "shared" plugin. It is for this reason that extending the builtin plugin command to add a new flag is "cleaner" than creating a new command (see the Possible Workarounds section below). That is, I think wp plugin install [some-plugin] and wp plugin install [some-plugin] --shared for the 2 cases of installing directly to one site vs installing to the shared location and symlinking to that one site is "more elegant" than wp plugin install [some-plugin] and wp shared-plugin install [some-plugin].

I created a plugin that manages these "shared" plugins. Part of that plugin defines an extention of the WP-CLI plugin command so that I can do:

$ wp plugin install [some-plugin] --shared

My extension looks for the --shared flag and if not present, just calls parent::install() and exits (as above). If it is present, then it looks in the shared directory to see if the plugin is already there and if so, just symlinks that directory into the site and exits (after outputing a WP_CLI::success() message); if the plugin isn't already in the shared directory, it creates a directory for it and symlinks that directory into the local site and finally just calls parent::install() to do the actual installation (which results in the plugin's code being installed in the shared directory).

My plugin also extends wp plugin activate and wp plugin deactivate to allow plugins to be activated/deactivated "locally" (i.e., without changing active_plugins in wp_options), similar to Mark Jaquith's disable-plugins-when-doing-local-dev.php gist (also see his blog post about it WordPress local dev tips: DB & plugins). To do that, it adds a --locally flag to both (as well as a couple of additional sub-commands to remove plugins from the "locally" activated/deactivated state).

wp parser create: extending 3rd party command

.org uses the phpdoc-parser plugin to generate the WP Core Code Reference. phpdoc-parser defines a WP-CLI command (wp parser create) that is used to generate the code reference.

I have created a plugin that defines an extension of wp parser create that, among other things, allows it to be used to generate code references for plugins. Like the wp plugin install case above, it adds a few additional flags, which when present, alter how wp parser create is invoked.

In particular, wp parser create was written to take only a single arg with the full path to the source it is supposed to parse (e.g., the path to where the Core sources are located). In my extension, if a specific additional flag is present, it interprets $args as a list plugin directories in WP_PLUGINS_DIR of the current site, creates a tmp directory, symlinks those plugins into the tmp directory and then calls parent::create( path_to_tmp_directory ) so that multiple plugins can be parsed in one invokation.

Possible Workarounds

I've tried various alternative ways in 2.2.0+ to achieve the kinds of extensions I can do in 2.1.0, but they are either "less elegant" (in my opinion) or they don't actually work.

I know I could create a completely new command (e.g., wp shared-plugin install [some-plugin]) and use WP_CLI::runcommand() to invoke plugin install [some-plugin] after doing the symlink "setup" that accomplishes much the same thing, but I think it is "cleaner" to just extend the builtin plugin command.

I've also tried to use WP_CLI::add_hook( 'before_invoke:command' ), but the callback hooked that way doesn't get passed the $args and $assoc_args params of the command being invoked, which makes it unusable for my use cases since I wouldn't know whether my additional flag(s) where given on the command line.

If that callback were passed those params by reference, so that they could be modified when needed (e.g., removing my extension flags from $assoc_args and/or changing $args[0] to be the path of the tmp dir in the wp parser create use case) I think I could probably achieve most of what I did in 2.1.0 by extending a existing command class. What would not be possible using WP_CLI::add_hook() is to alter what is output by wp help command, which is a big drawback of using WP_CLI::add_hook() to achieve was I used to do by just extending the command's class.

Possible Solution

After a quick glance at the source for WP_CLI::add_command() in 2.1.0 and 2.2.0, the only difference I see is that 2.1.0 has:

// line 525
if ( $existing_command instanceof Dispatcher\CommandNamespace ) {
	$subcommands = $existing_command->get_subcommands();
	...
}

whereas 2.2.0 has:

// line 532
if ( false !== $existing_command ) {
	$subcommands = $existing_command->get_subcommands();
	...
}

If I extract vendor/wp-cli/wp-cli/php/class-wp-cli.php from the 2.2.0 phar, change that one line back to what it was in 2.1.0 and add that file back to the phar and then invoke any of my extended commands things work again.

Since I don't know the internals of WP_CLI very well, I don't know why that change was made in 2.2.0 and whether changing it back to what it was in 2.1.0 will have consequences given other changes that were made in 2.2.0; however, after changing it back, I've run a few other basic commands (e.g. wp plugin list, etc) that seem to work as expected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions