Custom CLI commands — the configbox custom namespace
- Version
- 3.x
- Updated
Add site-specific command-line commands to the ConfigBox CLI suite without touching core: one file
in the upgrade-safe customization layer declares your commands, and they appear under the reserved
custom namespace next to the stock commands. Typical uses: a nightly order export the host's cron
calls, a data-fix a support engineer runs once, an import from the shop's ERP — anything you would
otherwise script against the database directly, but with Kenedo booted and every ConfigBox helper at
hand.
All paths below are relative to the component root (on WordPress:
docroot/wp-content/plugins/configbox/app/). For the stock suite this extends, see
technical/com_configbox_cli_commands.md.
Platform status: all three hosts ship this. Your
commands.phpis identical on every host — same fields, same callback signature, same return semantics. Only two things differ, and both are the host's business, not yours: where the file lives (§1) and how the command is spelled (§1.1), because each console has its own naming convention.
1. The contract
Create one file in the customization layer:
<customization dir>/cli/commands.php
The customization dir is wherever KenedoPlatform::p()->getDirCustomization() points on your host:
Per platform (the overview's §1.1 table has the full story):
- WordPress:
wp-content/plugins/configbox-customization/cli/commands.php— the layer is its own plugin-sibling directory, not inside theconfigboxplugin, so plugin updates never touch it (no plugin registration needed; it is not a real WordPress plugin).- Joomla / standalone:
data/customization/cli/commands.phpinside the component.- Magento 2:
app/code/Rovexo/ConfigboxCustomizations/view/customizations/cli/commands.php(theRovexo_ConfigboxCustomizationsmodule's ConfigBox subtree). If that module does not exist yet, create it — aregistration.php, anetc/module.xmlsequencing afterRovexo_Configbox, and theview/customizations/tree is all it needs.
1.1 Two ways to run them, and what each host calls yours
Every command you define is reachable two ways, and you do nothing to get either.
Declared — each becomes a real command of the host's console: it shows up in the command list,
has its own --help built from your shortdesc/synopsis/longdesc, and tab-completes.
| Host | Your order-count becomes |
|---|---|
| Joomla | php cli/joomla.php configbox:custom:order-count |
| Magento 2 | bin/magento configbox:custom:order-count |
| WordPress | wp configbox custom order-count |
custom is a namespace on all three — a colon-separated one on the Symfony consoles, a
space-separated one on WP-CLI. The name you key in commands.php is the same everywhere; only
the separator differs.
Dispatched — the always-available fallback, where the name is an argument rather than part of the command:
php cli/joomla.php configbox:custom # list what this site installs
php cli/joomla.php configbox:custom order-count 30
bin/magento configbox:custom order-count 30
wp configbox custom status # WordPress: the listing/diagnostic lives here
WordPress differs in one way, and it is forced. Because
customis a WP-CLI namespace, it cannot also be a runnable command — WP-CLI refuses with "'wp configbox custom' can't have subcommands". So there is nowp configbox custom <name>dispatcher form separate from the declared one (they are the same spelling), and the listing/diagnostic lives atwp configbox custom status. That makesstatusa reserved command name on WordPress: define one and it will not be registered, andwp configbox custom statuswill tell you so.wp configbox customon its own gives WP-CLI's own namespace listing.
Why both. Declaring means the host reads your file while it is still building its console — on
every invocation. That is what buys the list entry and the help page. But if your file is broken,
declared commands can only vanish (there is nothing to report to yet — no output object exists,
and a warning at that point would corrupt machine-readable output such as list --format=json).
The dispatcher — configbox:custom on Joomla/Magento, wp configbox custom status on WordPress —
is where the failure is explained, in full, with the path and the parse error. So: if your
command is missing from the list, run that and it will tell you why.
It must return an array of command definitions, keyed by command name:
<?php
defined('CB_VALID_ENTRY') or die();
return array(
'order-count' => array(
'shortdesc' => 'Count orders placed in the last N days.', // one-liner for the listing
'synopsis' => '[<days>]', // optional, shown in the listing
'longdesc' => "Counts order records created in the last N days.\n\n" // optional, the --help body
. " <days> how far back to look (default 30)",
'callback' => function (array $args, array $options) {
$days = isset($args[0]) ? max(1, (int) $args[0]) : 30;
$db = KenedoPlatform::getDb();
$db->setQuery("SELECT COUNT(*) FROM `#__cbcheckout_order_records`
WHERE `created_on` >= DATE_SUB(NOW(), INTERVAL " . (int) $days . " DAY)");
echo $db->loadResult() . "\n";
return 0; // exit code; null counts as 0
},
),
);
Run it — declared (its own command, with a help page) or dispatched (the fallback), see §1.1:
# Joomla # Magento 2 # WordPress
php cli/joomla.php configbox:custom:order-count 7
bin/magento configbox:custom:order-count 7
wp configbox custom-order-count 7
php cli/joomla.php configbox:custom # the dispatcher: list what this site installs
wp configbox custom order-count 7 # the dispatcher: run one
Per definition:
| Key | Required | Meaning |
|---|---|---|
| (array key) | yes | The command name — lowercase letters, digits and hyphens, starting with a letter or digit (order-export, not Order_Export). |
callback | yes | Any PHP callable. Receives (array $args, array $options), prints its own output (echo), returns an int exit code (null/no return counts as 0). |
shortdesc | no | One-line description, shown in the custom listing. |
synopsis | no | A usage hint for your arguments (e.g. <days> [--format=<fmt>]), shown in the listing and at the top of the help page. Documentation only — nothing is validated against it. |
longdesc | no | The help body, free-form text. Shown by --help (Joomla/Magento) and wp help configbox custom <name> (WordPress). This is where the real explanation goes: what the command does, what each option means, what it costs to run. Without it a declared command's help page has a one-line description and nothing else. |
The callback's inputs:
$args— the positional arguments after the command name, in order, as strings (wp configbox custom order-count 7→array('7')).$options— the--key=valueoptions askey => valuestrings; a bare--flagarrives askey => true.
The callback's environment: Kenedo is fully booted before any custom command runs — the database
(KenedoPlatform::getDb()), every ConfigBox model/helper, and the platform layer are available, and
outstanding migrations have been applied by the boot as usual. The command runs as a guest; if it
must act as a user, resolve and set one explicitly
(ConfigboxCliHelper::resolveUserId() + ConfigboxUserHelper::setUserId()), the same way the stock
run-task command does.
2. Failure isolation — why a broken file cannot break the stock suite
Declaring changed when your file is read, not whether it is contained. Because declared commands must exist before the console runs, the host now reads
commands.phpon everyconfigbox-related invocation, not only when a custom command is called. Every one of those reads is wrapped incatch (Throwable)— and a PHP parse error is a catchableParseError, so even an unparsable file costs you your custom commands and nothing else. The guarantee below is unchanged; it simply now has two places to hold rather than one. Verified on all three hosts: withcommands.phpdeliberately unparsable, the declared commands disappear from the list andconfigbox:migrate --statusstill runs.
The commands file is loaded only when the custom dispatcher itself is invoked, and always behind
a catch-everything guard. The stock commands (cache-clear, migrate, …) never load customization
code. Consequences you can rely on:
- A
commands.phpthat throws at load, violates the contract, or even has a PHP parse error breaks onlywp configbox custom …— with a message naming the file and the violation. Every stock command keeps working, so the tooling you deploy and recover with is never taken down by the code you are debugging. - A single command's callback throwing is contained too: the dispatcher reports
Custom command "<name>" failed: <message>and exits non-zero. Other custom commands are unaffected. - Contract violations are rejected with precise messages (non-array return, bad command name,
missing/uncallable
callback) rather than half-registering.
There is deliberately no WP-CLI per-command registration of custom commands: registering them as
first-class subcommands would mean executing customization code on every wp invocation, which is
exactly the coupling this design avoids.
3. Conventions and gotchas
- Exit codes are the API for scripts. Return
0for success and a non-zerointfor failure so cron jobs and deploy scripts can gate on your command (wp configbox custom nightly-export || alert). The dispatcher passes your code through (WP_CLI::halt()). - Print raw, parseable output for anything a script consumes (one value per line, or CSV/JSON) — the dispatcher does not wrap or decorate your stdout.
- Avoid WP-CLI's global option names. WP-CLI consumes
--user,--url,--path,--debug,--quietand the other global parameters before your command sees them — name your options something else (the stock suite renamed its acting-user selector to--cb-userfor the same reason). - Option values are strings; flags are
true. Cast numerics yourself; check flags with!empty($options['flag']). - Follow the Kenedo idioms for anything touching the database: manually built mysqli through
KenedoPlatform::getDb(),(int)casts /getEscaped()for values — same escaping discipline as neighboring core code. No namespaces, no PSR-4. - Long-running work: a CLI process has no web timeout, but migrations-style bookkeeping (locks,
idempotency) is on you. For schema changes, don't do them in a command — use the customization
migration track (
data/customization/updates/, seetechnical/com_configbox_migrations.md). - Names collide only within your own file. The
customnamespace is reserved for the customization layer, so a ConfigBox update can never introduce a stock command that shadows yours.
4. How it resolves in code (for the curious)
| Piece | Where | Role |
|---|---|---|
| Declaration (per host) | Joomla: the console plugin's registerCommands. Magento: a preference on CommandListInterface — a plugin is never called, the CLI builds its command list before interception is active. WordPress: WP_CLI::add_command() at plugin load, from a raw read, because Kenedo is not booted that early. | Each host's own wiring; the contract they feed is the same. |
| Contract + loader + runner | helpers/cli.php — ConfigboxCliHelper::getCustomCliCommandsPath() / loadCustomCliCommands() / executeCustomCliCommand() | Platform-agnostic core: path (getDirCustomization() . '/cli/commands.php' — resolves per host, see §1), validation, exit-code normalization. Shared by all hosts. |
| WordPress dispatcher | cli.php (adapter, cbx-wordpress repo) — ConfigboxCliCommand::custom() | Boots Kenedo, lazily loads the file behind catch (Throwable), lists or dispatches, maps exceptions/exit codes to WP-CLI. |
| Registration | configbox.php (adapter) | Registers only the dispatcher (wp configbox custom), like any stock subcommand. |
See also
technical/com_configbox_cli_commands.md— the stock CLI suite this extends, including the architecture split (shared core vs. thin host wrappers) your commands plug into.com_configbox_customization_overview.md— the extension-point map; this is one row of it.technical/com_configbox_migrations.md— the customization migration track, for delivering any schema your commands rely on.com_configbox_events_and_observers.md— prefer an observer when you want to react to something happening in the app; a CLI command is for work an operator or cron initiates.