Skip to main content
Version: 4.0 preview

Custom CLI commands — the configbox custom namespace

Version
4.0 preview
Updated
View markdown

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.php is 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 the configbox plugin, so plugin updates never touch it (no plugin registration needed; it is not a real WordPress plugin).
  • Joomla / standalone: data/customization/cli/commands.php inside the component.
  • Magento 2: app/code/Rovexo/ConfigboxCustomizations/view/customizations/cli/commands.php (the Rovexo_ConfigboxCustomizations module's ConfigBox subtree). If that module does not exist yet, create it — a registration.php, an etc/module.xml sequencing after Rovexo_Configbox, and the view/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.

HostYour order-count becomes
Joomlaphp cli/joomla.php configbox:custom:order-count
Magento 2bin/magento configbox:custom:order-count
WordPresswp 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 custom is 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 no wp configbox custom <name> dispatcher form separate from the declared one (they are the same spelling), and the listing/diagnostic lives at wp configbox custom status. That makes status a reserved command name on WordPress: define one and it will not be registered, and wp configbox custom status will tell you so. wp configbox custom on 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:

KeyRequiredMeaning
(array key)yesThe command name — lowercase letters, digits and hyphens, starting with a letter or digit (order-export, not Order_Export).
callbackyesAny PHP callable. Receives (array $args, array $options), prints its own output (echo), returns an int exit code (null/no return counts as 0).
shortdescnoOne-line description, shown in the custom listing.
synopsisnoA 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.
longdescnoThe 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 7array('7')).
  • $options — the --key=value options as key => value strings; a bare --flag arrives as key => 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.php on every configbox-related invocation, not only when a custom command is called. Every one of those reads is wrapped in catch (Throwable) — and a PHP parse error is a catchable ParseError, 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: with commands.php deliberately unparsable, the declared commands disappear from the list and configbox:migrate --status still 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.php that throws at load, violates the contract, or even has a PHP parse error breaks only wp 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 0 for success and a non-zero int for 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, --quiet and the other global parameters before your command sees them — name your options something else (the stock suite renamed its acting-user selector to --cb-user for 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/, see technical/com_configbox_migrations.md).
  • Names collide only within your own file. The custom namespace 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)

PieceWhereRole
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 + runnerhelpers/cli.phpConfigboxCliHelper::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 dispatchercli.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.
Registrationconfigbox.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.