| name | add-cli-command |
| description | Register a WP-CLI command in a plugin using a command class, following Dekode conventions. |
Add a WP-CLI Command
Register a WP-CLI command in an existing plugin. Covers command class structure, subcommand pattern, argument handling, and gating so the class is never loaded on web requests.
Inputs
| Input | Required | Notes |
|---|
| Plugin | Yes | Which plugin in packages/plugins/ |
| Command name | Yes | Top-level command, e.g. dekode-sync. Registered as wp {command-name} |
| Subcommands | Yes | List of subcommands and what each does, e.g. run, status, reset |
| Arguments / flags | No | Positional args or --flag options each subcommand accepts |
| Namespace | Yes | Check the target package — e.g. Dekodeinteraktiv\Plugin\<PascalSlug>\Cli |
If any required inputs are missing, ask for them before writing any code.
Step 1 — Create the command class
Create src/Cli/<CommandName>Command.php in the target plugin.
<?php
declare( strict_types=1 );
namespace {PhpNamespace}\Cli;
class <CommandName>Command {
public function run( array $args, array $assoc_args ): void {
$dry_run = (bool) ( $assoc_args['dry-run'] ?? false );
\WP_CLI::log( 'Starting sync...' );
if ( $dry_run ) {
\WP_CLI::success( 'Dry run complete — no changes applied.' );
return;
}
\WP_CLI::success( 'Sync complete.' );
}
public function status( array $args, array $assoc_args ): void {
$format = $assoc_args['format'] ?? 'table';
$items = [
[ 'key' => 'last_run', 'value' => \get_option( '{plugin-slug}_last_run', 'never' ) ],
[ 'key' => 'status', 'value' => \get_option( '{plugin-slug}_status', 'idle' ) ],
];
\WP_CLI\Utils\format_items( $format, $items, [ 'key', 'value' ] );
}
}
Step 2 — Register the command via hooks()
Gate the registration so the command class is never loaded on web requests:
public function hooks(): void {
if ( defined( 'WP_CLI' ) && \WP_CLI ) {
add_action( 'after_setup_theme', [ $this, 'register_cli_commands' ] );
}
}
public function register_cli_commands(): void {
\WP_CLI::add_command( '{command-name}', new \{PhpNamespace}\Cli\<CommandName>Command() );
}
Use after_setup_theme rather than init — WP-CLI typically loads before init fires for some operations, and after_setup_theme ensures WordPress is sufficiently bootstrapped.
Step 3 — WP_CLI output helpers
Use the appropriate WP_CLI method for each output type — never echo or var_dump:
| Method | When to use |
|---|
WP_CLI::log( $msg ) | Informational progress messages |
WP_CLI::success( $msg ) | Final success message (green) |
WP_CLI::warning( $msg ) | Non-fatal issue (yellow) |
WP_CLI::error( $msg ) | Fatal error — halts execution (red) |
WP_CLI::error( $msg, false ) | Non-fatal error — continues execution |
WP_CLI\Utils\format_items() | Tabular data with --format support |
WP_CLI\Utils\make_progress_bar() | Progress bar for long-running loops |
Step 4 — Lint
composer lint
composer lint-fix
composer lint
Conventions
- Gate with
defined( 'WP_CLI' ) && WP_CLI: the command class and its dependencies must not be instantiated on web requests
- Command name prefix: use a project or organisation prefix (e.g.
dekode-sync, not just sync) to avoid conflicts with Core and other plugins
- PHPDoc
## OPTIONS / ## EXAMPLES: WP-CLI parses these docblocks to generate wp help {command-name} {subcommand} output — always include them
--format flag for data output: any command that outputs a list of items should support --format=table,json,csv via WP_CLI\Utils\format_items()
--dry-run flag for destructive operations: any command that writes, deletes, or modifies data should support a --dry-run flag
- No hardcoded option keys: use the same constants or key functions used elsewhere in the plugin
- Exit codes:
WP_CLI::error() exits with code 1; WP_CLI::success() exits with 0 — do not call exit() directly
Output
A src/Cli/<CommandName>Command.php class with documented subcommands, registered via hooks() behind a WP_CLI gate. Each subcommand is callable as wp {command-name} {subcommand} and outputs correctly formatted results. Lint passes clean.