How to create a command that holds other commands in wp-cli (WordPress CLI)

Almost every major site I work on has at least one custom wp-cli command that I’ve built to manage or automate things. But I always run into the same problem, I don’t want my command to be a root command, I want it to live in a namespace of my own. But, I also don’t want to create a blank command that doesn’t do anything just for the sake of this namespace.

For instance, if I register a command using this:

WP_CLI::add_command('custom-command do-thing', DoThingCommand::class);

WordPress will show the below if I just type wp:

  cache                 Adds, removes, fetches, and flushes the WP Object Cache object.
  cap                   Adds, removes, and lists capabilities of a user role.
  cli                   Review current WP-CLI info, check for updates, or see defined aliases.
  comment               Creates, updates, deletes, and moderates comments.
  config                Generates and reads the wp-config.php file.
  core                  Downloads, installs, updates, and manages a WordPress installation.
  cron                  Tests, runs, and deletes WP-Cron events; manages WP-Cron schedules.
  custom-command        
  db                    Performs basic database operations using credentials stored in wp-config.php.
  embed                 Inspects oEmbed providers, clears embed cache, and more.
  eval                  Executes arbitrary PHP code.
  eval-file             Loads and executes a PHP file.

If you don’t notice or are unable to see the problem, each command listed has an “intro” description except for my command which instead just has a blank space.

Instead of grepping all of wp-cli’s code, I luckily had Yoast installed, too, so I grepped their code and saw a special class from core that you can extend call CommandNamespace which has the following comment:

Adds a command namespace without actual functionality.

This is meant to provide the means to attach meta information to a namespace when there’s no actual command needed.

In case a real command gets registered for the same name, it replaces the command namespace.

https://github.com/wp-cli/wp-cli/blob/bc37f66e25b9992813b0c50b933aefbf88718e6a/php/WP_CLI/Dispatcher/CommandNamespace.php#L8

So, just create a class that extends that (and probably make it final), register it like a normal command, and WordPress will do the rest for you.