fof/console
FoF Console adds a browser-based console to the Flarum admin panel, letting administrators run flarum commands without SSH. Useful for cache clearing, maintenance, and extension management, with command output shown directly in the dashboard.
1. Confirm you actually need it. fof/console was archived in March 2021, has 3 stars, zero dependents, and was last tagged against an early Flarum 1.0-era core. Before installing, run php flarum list on your target Flarum version and check whether core already ships a console/command extender — this package is the kind of thing that gets absorbed upstream and then abandoned. If core covers it, use core.
2. Install (if you're on a matching core).
composer require fof/console
php flarum cache:clear
Then enable the extension in the admin panel (/admin#/extensions) or via php flarum extension:enable fof-console.
3. Read three things, in this order:
extend.php / src/Extend/ in the package — this is the entire public API surface (typically an EnableConsole extender plus a RegisterConsoleCommand extender).src/Console/Command.php — the base class. It's tiny; it tells you whether you implement handle() (Flarum convention) or execute() (raw Symfony), and what $this->app/container accessor you get.vendor/flarum/core/src/Console/AbstractCommand.php — because the base class here almost certainly extends it, and that is where your info()/error() helpers and the container actually come from.4. First use case — a safe, read-only command. Don't start with a destructive myext:purge. Start with something you can verify by eye:
// extend.php
use FoF\Console\Extend\EnableConsole;
use FoF\Console\Extend\RegisterConsoleCommand;
use MyVendor\MyExt\Console\StatsCommand;
return [
new EnableConsole(),
new RegisterConsoleCommand(StatsCommand::class),
];
// src/Console/StatsCommand.php
namespace MyVendor\MyExt\Console;
use FoF\Console\Console\Command;
class StatsCommand extends Command
{
protected $name = 'myext:stats';
protected $description = 'Print discussion/post counts for a quick health check';
protected function handle(): void
{
$this->info('Discussions: ' . \Flarum\Discussion\Discussion::count());
$this->info('Posts: ' . \Flarum\Post\Post::count());
}
}
Verify with php flarum list | grep myext and php flarum myext:stats -vvv.
Namespace-prefix every command. Flarum's flarum binary is a flat namespace shared by core (migrate, cache:clear, extension:enable, settings:set, …) and every other extension. Always use vendor:action (acme:reindex, acme:prune-attachments). Collisions are silent-ish and confusing, and you cannot rename a command after users have it in cron.
Thin command, fat service. Put the logic in a normal injectable class and have the command do argument parsing + output only. This makes it testable without booting the console, and lets the same logic back an API endpoint or a job:
class ReindexCommand extends Command
{
protected $name = 'acme:reindex';
protected $description = 'Rebuild the Acme search index';
protected function handle(): void
{
$service = $this->app->make(\Acme\Search\Indexer::class);
$service->reindexAll(fn ($n) => $this->info("indexed {$n}"));
}
}
Always ship a non-interactive path. Console commands are cron targets. Support --force, --dry-run, and --limit=N, and never require a prompt. Symfony's --no-interaction/-n flag exists, but you should not depend on users passing it — check $this->input->isInteractive() before asking.
Return exit codes. Wrap the body in a try/catch, $this->error($e->getMessage()) on failure, and return 1 (or Command::FAILURE). Uncaught exceptions produce a stack trace and a non-zero code, which is fine for humans and bad for monitoring. Never call exit()/die() — it skips Flarum's shutdown and leaves locks/caches inconsistent.
Resolve paths from the Flarum root, not getcwd(). Cron will happily invoke you from /. Use the base path from the application instance (e.g. $this->app->basePath() / Application::getBasePath()) for any file I/O.
Use it as glue for ops tasks, not as a second API. The realistic day-to-day patterns:
| Task | Why console beats an endpoint |
|---|---|
| Backfill a new column after a migration | Runs before the extension is enabled/UI exists |
| Re-encrypt / rotate keys | No request timeout, no HTTP exposure |
| Warm a cache after deploy | Idempotent, callable from CI |
| Bulk moderation / data repair | No permission or rate-limit surface |
| Import from a legacy forum | Long-running, resumable via --offset |
Wire it into CI/deploy instead of ad-hoc php -r. php flarum acme:warm-cache --no-interaction in your deploy script, gated on set -e. Add --verbose in staging for the trace.
Compose with core commands. Your command can shell out to or mirror extension:enable, migrate, cache:clear semantics — but prefer calling the same services those commands call rather than exec('php flarum ...'), which spawns a second boot and can deadlock on the same DB/lock files.
Archived = frozen, not stable. No security patches, no Flarum 2.x compatibility, no fixes for newer illuminate/console/Symfony Console versions. Check composer why-not fof/console and be prepared to fork it into repositories in composer.json, or just drop it and register commands through core's extender. Vendor-forking a 200-line package is often cheaper than fighting the constraint.
Extenders only run when the extension is enabled. This bites hard in CI: a fresh install with the extension disabled has no commands, so your deploy script's php flarum acme:reindex fails with "Command not found." Either make the pipeline extension:enable first, or accept that the command is only available post-enable and document it.
New files need a fresh autoload. Adding src/Console/MyCommand.php and then hitting "Command not found" is nearly always composer dump-autoload (or a stale opcache), not a bad registration. Also run php flarum cache:clear after touching extend.php.
Duplicated registration throws. Registering the same command class twice (e.g. from two code paths, or an extender re-evaluated after an extension re-enable) gets you a Symfony "command already exists" error at boot. Register once, in one extend.php.
Commands bypass the API's permission layer entirely. There's no actor, no policy check, no CSRF. A command you write has full DB access as the CLI user. Guard destructive commands with an explicit flag plus a confirmation when interactive, and print what you're about to delete before deleting it. Consider refusing to run destructive operations when --no-interaction is set without --force.
The console environment is not the web environment. Some extensions register service providers conditionally, settings may resolve from a different path, and queue/cache drivers can differ. If behaviour diverges from the web UI, check the environment detection in Flarum\Foundation\Console\* and what your config.php says for the CLI. Also: the CLI user may have different filesystem permissions than PHP-FPM — a command that writes to storage/ can fail where the web app succeeds.
Debugging ladder: php flarum list (did it register?) → php flarum acme:foo --help (are args wired?) → php flarum acme:foo -vvv (trace) → temporarily var_dump in handle() and run with --no-interaction to rule out prompt hangs. A command that "just hangs" is almost always blocked on an interactive question or a queue/db lock.
Extension points to prefer: override configure() for arguments/options rather than parsing $this->input by hand; use the base class's container accessor for services instead of app()-style globals; use Symfony's Table/ProgressBar helpers (or $this->output + SymfonyStyle) for output rather than echo, so --quiet and piping to jq/grep behave.
How can I help you explore Laravel packages today?