asika/simple-console
Lightweight PHP library for building simple command-line applications. Define commands, parse arguments and options, format console output, and handle user input without a full framework—ideal for small CLI tools, scripts, and maintenance utilities.
asika/simple-console is a single-file, dependency-free CLI kernel. There is no application container, no command registry, and no auto-generated help — you subclass Asika\SimpleConsole\Console, implement doExecute(), and get argv parsing, stdout/stderr helpers and exit codes for free.
1. Install (or vendor the file):
composer require asika/simple-console
Because the whole framework is one file (src/Console.php), you can also just copy it into bin/ for zero-dependency build scripts.
2. Read the source first. It is roughly 300–500 lines. Skim it once, top to bottom, and you will know the entire API — getArgument(), getOption(), out(), err(), execute() and the option-parsing regex. There is nothing hidden behind traits or service providers.
3. Smallest working command:
#!/usr/bin/env php
<?php
require __DIR__ . '/vendor/autoload.php';
use Asika\SimpleConsole\Console;
class GreetCommand extends Console
{
protected $help = <<<HELP
Usage: greet [name] [-u|--upper]
Say hello.
HELP;
protected function doExecute(): int
{
$name = $this->getArgument(0, 'World');
$msg = "Hello, {$name}!";
if ($this->getOption('u') || $this->getOption('upper')) {
$msg = strtoupper($msg);
}
$this->out($msg);
return 0;
}
}
exit((new GreetCommand($argv))->execute());
php greet.php Alice --upper
4. First real use case: replace one-off shell glue. A bin/deploy.php, bin/db-reset.php or a Makefile replacement that needs arguments, flags, prompts and a meaningful exit status is exactly the sweet spot. If you would otherwise write parse_str/getopt() boilerplate, this package pays for itself in ten lines.
One class per command, a base class for bootstrap. Put shared setup (autoload, config load, DB connection, logger) in an abstract AppCommand extends Console, then have each command extend that. Keep doExecute() free of exit() calls so it stays testable.
abstract class AppCommand extends Console
{
protected function boot(): void { /* load config */ }
protected function doExecute(): int { $this->boot(); return $this->handle(); }
abstract protected function handle(): int;
}
Argument vs option discipline. Positional arguments are read by index (getArgument(0), getArgument(1, $default)); options by name (getOption('v') or getOption('verbose')). Decide early whether a flag is a boolean switch or takes a value (--env=prod), and document it in $help. Treat getArgument() indexes as part of your public contract — reordering them is a breaking change for anyone's CI script.
Subcommand dispatcher. The package has no router, so the standard pattern is a single front controller that peeks at $this->getArgument(0) and delegates:
$map = [
'build' => BuildCommand::class,
'deploy' => DeployCommand::class,
];
$name = $this->getArgument(0, 'list');
if (!isset($map[$name])) {
$this->err("Unknown command: {$name}");
return 1;
}
return (new $map[$name]($this->input))->execute();
This keeps the tiny framework tiny and gives you a clean bin/app <command> UX.
Interactive scripts with a --no-interaction escape hatch. Ask only when stdin is a TTY and the user hasn't opted out. Every prompt should have a flag that supplies the value instead, otherwise your script can never run in CI.
$env = $this->getOption('env');
if ($env === null) {
if ($this->getOption('n')) {
$this->err('--env is required in non-interactive mode');
return 1;
}
$env = $this->ask('Target environment:');
}
Stream separation. Route machine-readable results and progress to out() (stdout) and diagnostics/warnings to err() (stderr). That single convention makes php bin/report.php | jq work and keeps CI logs readable. Honour a quiet flag by guarding every progress line.
Composer script integration. Wire commands into composer.json and remember the -- separator — Composer strips everything before it:
{ "scripts": { "release": "php bin/console.php build --minify" } }
composer release -- --env=staging
Testing. Because the constructor takes an argv array, you can drive commands without a subprocess. Instantiate with a hand-built array, run execute(), assert on the returned int and on captured output streams. This is the main reason to return codes instead of calling exit().
Packaging as a phar or standalone bin. The framework's lack of runtime dependencies makes it ideal for a box/php-scoper phar. Since it is one MIT file, you can also inline it and ship a truly single-file tool.
$argv (with the script name at index 0) and handles slicing internally — but confirm this in src/Console.php before building arrays for tests. Off-by-one on getArgument(0) is the single most common source of "why is my argument empty".-v, -abc bundling, --verbose and --key=value. Do not assume --key value (space-separated) works. If you need that, read the value from the next positional argument yourself or normalise argv before constructing the command.--verbsoe may be silently ignored. If your command's correctness depends on a flag, validate at the top of doExecute() and fail loudly with a non-zero code.--help generation. You are responsible for the $help string and for handling -h. Keep it short but include usage, every option, and one example — future-you reading CI logs will thank you.0 on success, non-zero on failure, and never exit() inside doExecute() — that kills testability and skips any cleanup. Let the thin bin/ wrapper do exit($cmd->execute()).stream_isatty(STDOUT) is true; otherwise plain text. Colour codes leaking into GitHub Actions or Jenkins logs is a classic self-inflicted wound.out() usually appends a newline. Don't add \n yourself or you get double-spaced logs; conversely, use whatever raw-write path the class offers for progress bars that need \r.composer.json and check the latest release notes (last tag 2025-06-06). With only 26 stars and near-zero dependents, the ecosystem feedback loop is thin — treat minor bumps as potentially breaking and lock the version in CI-heavy projects.Asika\SimpleConsole, Composer will warn or you'll get a duplicate-class fatal. Only do one or the other.doExecute() for logic, override execute() if you need pre/post hooks or global exception-to-exit-code translation, and use the $input/$output properties for anything the helpers don't cover. If you find yourself wanting a container, events or command discovery, that is the signal to graduate to Symfony Console rather than to bolt those features onto this package.How can I help you explore Laravel packages today?