anvilm / iva
CLI framework for PHP 8.4+ with a real command tree, strongly typed input DTOs, GNU/POSIX-style option parsing, and a section/progress-bar/spinner output model.
Requires
- php: ^8.4
- php-standard-library/ansi: ^6.0
- php-standard-library/async: ^6.0
- php-standard-library/date-time: ^6.0
- php-standard-library/foundation: ^6.0
- php-standard-library/io: ^6.0
- php-standard-library/os: ^6.0
- php-standard-library/str: ^6.0
- php-standard-library/type: ^6.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Iva
A modern, strongly-typed CLI framework for PHP 8.4+
Important
This entire library — every line of source code, every docblock, and this README — was written by an AI. No human has hand-written the implementation. Review the code carefully before relying on it in production, and please open an issue if you spot a bug or a design flaw.
Overview
Iva is a command-line application framework for PHP. It gives you a real, nestable command tree (think
git remote add), strongly-typed input DTOs instead of stringly-typed $input->getOption('name') lookups, a
GNU/POSIX-compliant option parser, and a rich output model with sections, progress bars, spinners, tables and
trees — all without reflection, attributes, or hidden global state.
Two instances of Application can coexist in the same process, commands are built and registered explicitly, and
every value you read back in execute() is read through the very same typed Argument/Option object you declared
in configure() — so a typo or a type mismatch is a static-analysis error, not a runtime surprise.
Features
- Real command tree — unlimited nesting (
app db migrate up), grouping nodes that just list their children (likegit remote), aliases, hidden commands, and automatic "did you mean...?" suggestions for typos. - Statically-typed input —
Argument::string(),Option::int(),Option::flag(), etc. return a typed handle that you keep as a property and read back with$input->argument($this->name)/$input->option($this->loud)— no magic strings, nomixed. - GNU/POSIX-style parsing — long options (
--verbose), short flags and their bundling (-vvv),--opt=value/--opt value, negatable flags (--no-foo), variadic arguments, the--end-of-options marker, and optional option-name abbreviation. - Rich output model — colored/styled text (
<error>,<comment>, custom styles), verbosity levels (-q,-v,-vv,-vvv), redrawable sections, progress bars (determinate & indeterminate), spinners, tables, and trees, with automatic ANSI/color-support detection. - Command lifecycle hooks —
PreRun/PostRunon a command, andPersistentPreRun/PersistentPostRunon any ancestor group, run like afinallyblock even when the command throws. - Auto-generated help — usage lines, argument/option tables and examples are generated from the same definitions
used to parse input, for the whole app, a group, or a single command (
--help/-h). - First-class testing utilities —
CommandTesterandApplicationTesterrun a command through the real lexer → parser → binder pipeline against aBufferedOutput, with no real process orexecinvolved. - Persistent (inherited) options — declare an option once on a parent group and every subcommand can read it back through the same typed handle.
Requirements
| Requirement | Version |
|---|---|
| PHP | ^8.4 |
Iva depends on the php-standard-library packages (foundation, type,
str, io, ansi, async, os, date-time) — installed automatically via Composer.
Installation
composer require anvilm/iva
Quick Start
<?php declare(strict_types=1); require __DIR__ . '/vendor/autoload.php'; use src\Application;use src\Command\Command;use src\Input\Argument;use src\Input\Input;use src\Input\Option;use src\Output\Output; final class GreetCommand extends Command { private Argument $name; private Option $loud; protected function configure(): void { $this->setName('greet'); $this->setDescription('Greets someone by name'); $this->name = $this->addArgument(Argument::string('name', 'Who to greet')); $this->loud = $this->addOption(Option::flag('loud', shortcut: 'l', description: 'Shout the greeting')); $this->addExample('greet Ada --loud', 'Greets Ada, loudly'); } public function execute(Input $input, Output $output): int { $name = $input->argument($this->name); $greeting = sprintf('Hello, %s!', $name); $output->writeln($input->option($this->loud) ? strtoupper($greeting) : $greeting); return 0; } } $app = new Application('greet-app', '1.0.0'); $app->command(new GreetCommand()); exit($app->run($argv));
$ php greet.php Ada --loud
HELLO, ADA!
Core Concepts
Commands
Every command extends Command, declares itself in configure(), and receives an already-parsed, already-typed
Input in execute():
final class DeployCommand extends Command { protected function configure(): void { $this->setName('deploy'); $this->setDescription('Deploys the application'); $this->setAliases(['d']); } public function execute(Input $input, Output $output): int { // ... return ExitCode::Ok->value; } }
Nested commands & groups
Build a subcommand tree with CommandBuilder::subcommand(). A CommandGroup has no behaviour of its own — running
it just lists its children, like git remote:
final class DbGroup extends CommandGroup { protected function configure(): void { $this->setName('db'); $this->setDescription('Database commands'); } } $app->command(new DbGroup()) ->subcommand(new MigrateCommand()) ->subcommand(new SeedCommand());
$ php app.php db migrate --help
Arguments & Options
Arguments and options are built through named constructors and kept as typed handles:
protected function configure(): void { $this->setName('copy'); $this->source = $this->addArgument(Argument::string('source')); $this->targets = $this->addArgument(Argument::string('targets', variadic: true)); $this->force = $this->addOption(Option::flag('force', shortcut: 'f', negatable: true)); $this->retries = $this->addOption(Option::int('retries', default: 3)); $this->tags = $this->addOption(Option::strings('tag')); }
Persistent options declared on a parent group are inherited by every subcommand and can be read back through the
same shared handle — see the docblock on Command::addOption() for the pattern.
Output
public function execute(Input $input, Output $output): int { $output->writeln('<comment>Starting...</comment>'); $bar = $output->progressBar(100); $bar->start(); for ($i = 0; $i < 100; $i++) { $bar->advance(); } $bar->finish(); $output->table() ->headers(['Name', 'Status']) ->rows([['web', 'running'], ['worker', 'stopped']]) ->render(); $spinner = $output->spinner('Connecting...'); $spinner->start(); // ... $spinner->finish(); return ExitCode::Ok->value; }
Verbosity is controlled by the built-in -q, -v, -vv, -vvv global flags, and $output->isVerbose() /
$output->isDebug() let a command adapt its own logging.
Lifecycle hooks
final class DbGroup extends CommandGroup implements PersistentPreRun, PersistentPostRun { public function persistentPreRun(Output $output): void { /* open a connection */ } public function persistentPostRun(Output $output, int $exitCode): void { /* always close it */ } }
PersistentPostRun runs like a finally block: it fires even if the subcommand throws.
Testing
use src\Testing\CommandTester; $tester = new CommandTester(new GreetCommand()); $tester->execute(['Ada', '--loud']); $tester->assertSuccessful(); $tester->assertOutputContains('HELLO, ADA!');
ApplicationTester does the same at the whole-application level, resolving the command path exactly as run()
would.
Supported input styles
| Style | Example |
|---|---|
| Long option | --verbose |
| Long option with value | --timeout=30 / --timeout 30 |
| Short flag | -v |
| Bundled short flags | -vvv |
| Negatable flag | --no-force |
| Variadic argument | cmd file1 file2 file3 |
| End-of-options marker | cmd -- --not-an-option |
Project Structure
src/
├── Application.php # Entry point: parses argv, resolves & runs a command
├── ExitCode.php # Standard sysexits-style exit codes
├── Command/ # Command tree: Command, CommandGroup, CommandBuilder, lifecycle hooks
├── Input/ # Lexer, Parser, Definition, Argument, Option, typed InputType system
├── Output/ # Output, ConsoleOutput, sections, ProgressBar, Spinner, Table, Tree, formatter
├── Help/ # Auto-generated help & usage lines
└── Testing/ # CommandTester, ApplicationTester
License
The project is distributed under the MIT License. For details, refer to the LICENSE file.