WP-CLI commands
Some tasks don't belong in a web request: importing a feed with thousands of entries, cleaning up old data, migrating content after a change of its structure. WP-CLI runs such tasks from the command line, without a time limit and without a browser.
A plain WP-CLI command is a class or closure that you register with WP_CLI::add_command(). It receives its arguments as an array of strings, and its help text and the rules for its arguments live in a doc comment that WP-CLI parses. WP-CLI only checks the form of the arguments: --limit=many reaches your code as the string "many".
With the framework, a command is a method with #[CliSubcommand]. The build derives the command line from the method signature, and the framework converts every value to the declared type before your method runs. Like a hook or a REST route, the class is a service: it gets its dependencies injected and is created only when the command runs.
Commands, groups and the namespace
Every command has three parts: wp {namespace} {command} {subcommand}. The namespace is the one of your theme or plugin (extra.wp-app-framework.namespace, see Concepts). The command is a group named by #[CliCommand] on a class, the subcommand a method of that class.
The namespace comes first, so commands of different themes and plugins never collide with each other. WP-CLI itself would not prevent that. A second command with an existing name is silently merged with the first one, and one of them loses its description or subcommands.
A first command
Start with a class that imports events. #[CliCommand] gives the group its name and a one-line description:
Each subcommand is a public method that returns void. #[CliSubcommand] names it and describes it in one line; longDescription adds text that the help shows after the options, typically examples:
With the namespace sections, this is the command wp sections events import <url> [--limit=<limit>] [--mode=<mode>] [--dry-run]. Read the signature from left to right:
CliOutput $outputreceives the output of the command; the framework passes it in.#[Argument] string $urlis a positional argument,<url>. It has no default, so it is required.#[Option] int $limit = 50is an option,--limit=<limit>. Without it, the method receives the default 50; with--limit=many, the command stops withError: Invalid value for --limit: limit is not of type integer.before your method runs.#[Option] ImportMode $modeis a backed enum. The help lists its values, and only these are accepted; the method receives the enum case.#[Option] bool $dryRun = falseis a flag,--dry-run. WP-CLI also understands--no-dry-run.
A parameter typed with a service, such as a repository, is injected like in a hook. Each parameter must be one of these kinds; a parameter that is neither the output nor a service nor marked with #[Argument] or #[Option] is a build error. The marker is required so that a typo in a type can't turn a service into an argument without you noticing.
Arguments and options
Parameter names become kebab-case: $dryRun and $dry_run both become --dry-run, so they can't appear in the same method. The names can't be changed otherwise; rename the parameter instead.
A positional argument with a default value is optional, [<count>]. A variadic argument takes all remaining values and needs at least one:
An option without default value is required: WP-CLI stops with missing --mode parameter when it is left out. A nullable option or argument needs a default value, usually null; the method receives the default when the value is not given.
The values are checked and converted by the same rules as REST arguments: "5" becomes the integer 5, "0.5" the float 0.5, and a backed enum accepts exactly its values. WP-CLI itself would also accept a comma-separated list of enum values such as --mode=merge,replace; the framework rejects it, because the parameter takes one value.
Output and errors
Write output through CliOutput, never with echo, so that WP-CLI can control it:
--quiet suppresses the messages. CliOutput is passed to the method; it is no service, so a constructor can't ask for it.
To end a command with an error, throw a CliException. WP-CLI prints Error: {message} and exits with the exit code of the exception, 1 by default. Scripts that call your command can tell different failures apart by the exit code; it must be between 1 and 255.
Any other exception is unexpected. The framework writes it to the PHP error log with the command and the method. With WP_DEBUG enabled, it rethrows the exception, so PHP reports it with its stack trace; WordPress then ends the command with its message about a critical error. Without WP_DEBUG, the command ends with Error: Command failed: {exception class}: {message}. In both cases the exit code is 1, unless the site disables the fatal error handler of WordPress.
:::warning
- Development mode writes a cache. The framework builds the manifest on the first run and caches it in
wp-content/cache/{slug}/. Runwpas the user of the web server (e.g.docker compose exec -u www-data …), not with--allow-root: files that root creates there can't be updated by the web server afterwards. - Commands come with their theme or plugin. WP-CLI loads WordPress with the active theme and plugins, which register their commands. With
--skip-themesor--skip-plugins, the commands of the skipped consumers don't exist. - The namespace must be free. If another plugin or package already registers a command with the name of your namespace, the framework doesn't register your commands and prints a warning instead. The build rejects a namespace that is a command of WP-CLI itself, such as
postorcache. :::
Rules
The build reports, with class, method and parameter:
#[CliSubcommand]methods in a class without#[CliCommand], or#[CliCommand]without#[CliSubcommand]methods;- a class that is no service candidate; a method that is not public, is static or doesn't return
void; - a command or subcommand name that doesn't start with a lowercase letter followed by lowercase letters, digits and
-; a group name used twice in the project, a subcommand name used twice in a group; - a description that is empty, has a line break or more than 80 characters;
- a parameter that is neither
CliOutput, nor a service, nor marked; a parameter with both markers; a marker on the output or a service; - a type from the table that is not supported;
#[Param],#[PostReference],#[TermReference]or#[UserReference]; a by-reference parameter; a variadic option; - a nullable argument or option without default value; a flag that is not
bool $name = false; a variadic argument after an optional one; - two parameters with the same name in kebab-case; an option named like a global parameter of WP-CLI (
--url,--path,--user,--debug,--quiet…) or--help; an option starting withno-, which WP-CLI reads as negated flag; - a project namespace that is a command of WP-CLI, such as
post,cacheoroption, as soon as the project has commands.