Search by

anvilm / iva

AnvilM

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.

Package info

github.com/AnvilM/iva

pkg:composer/anvilm/iva

Statistics

Installs: 20

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.6 2026-10-25 20:41 UTC

This package is auto-updated.

Last update: 2026-10-25 20:42:42 UTC


README


logo
Iva

A modern, strongly-typed CLI framework for PHP 8.4+

php-version-shield license-shield

status-shield ai-written-shield

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 (like git 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, no mixed.
  • 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 / PostRun on a command, and PersistentPreRun / PersistentPostRun on any ancestor group, run like a finally block 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 — CommandTester and ApplicationTester run a command through the real lexer → parser → binder pipeline against a BufferedOutput, with no real process or exec involved.
  • Invocation trace — $input->invocation() exposes the command line exactly as it was typed: the order of every option and positional, argv positions, raw per-occurrence values, -vvv clusters, --no-x, abbreviations and the -- marker, with a chainable query API.
  • Aligned, wrapped help — descriptions are wrapped to the terminal width and every continuation line is aligned to the description column; items are separated by a blank line.
  • 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 Iva\Application;
use Iva\Command\Command;
use Iva\Input\Argument;
use Iva\Input\Input;
use Iva\Input\Option;
use Iva\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 Iva\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.

Invocation order

Values tell you what was passed; $input->invocation() tells you how and in which order. It returns an immutable InvocationLog of ArgvEntry objects, one per option occurrence (--tag a --tag b is two, -vvv is three), per positional token, and one for a -- marker.

public function execute(Input $input, Output $output): int
{
    $log = $input->invocation();

    // Typed order of related options: --include a --exclude b --include c
    foreach ($log->ofAny($this->include, $this->exclude) as $entry) {
        $rule = $entry->name === 'include' ? '+' : '-';
        $output->writeln($rule . ' ' . $entry->value);
    }

    $log->precedes($this->force, $this->dryRun);   // was --force typed before --dry-run?
    $log->last($this->color)?->isNegated();        // does the last --color/--no-color win?
    $log->options()->labels();                     // ['--force', '--tag', '--tag']
    $log->times('verbose');                        // -vvv => 3
    $log->afterTerminator()->values('file');       // tokens typed after `--`
    $input->provided($this->retries);              // typed explicitly, or just the default?

    echo $log->describe();                         // debug table
}

A target is an Option/Argument handle (typed, only matches that kind) or a string matched against the entry name or label ('tag', '--tag'). Every narrowing method returns a new log, so queries chain:

Method Purpose
all(), count(), iteration every entry in typed order
options(), arguments(), afterTerminator() narrow by kind
of($t), ofAny(...$ts) narrow to one or several targets, interleaved as typed
before($t), after($t), between($a, $b) slice around first occurrences
has, first, last, at, times, values lookups
positionOf, sequenceOf, precedes, follows positions and relative order
labels(), names(), groupedByName(), map(), filter(), slice(), toArray(), describe() projections
tokens(), offset() the raw argv tail the parser saw and how many tokens the command path used

Each ArgvEntry carries kind, name, token (the raw token), value (raw value of that occurrence; true/false for flags), position (0-based argv index after the script name, command path included), sequence, ordinal, form (Long, LongNegated, LongAbbreviated, Short, Positional, Terminated, Terminator), cluster (letter index inside -abc) and valuePosition (argv index of a separate value token, null when inline). A positional that no declared Argument claims keeps an empty name. CommandTester fills the log too, from the tokens you pass.

Help layout

--help renders arguments, options and commands as a two-column table. Descriptions are wrapped to the terminal width (COLUMNS, 80 when unknown, clamped to 40–120; $app->setHelpWidth(100) fixes it) and every continuation line is indented to the start of the description column, with one blank line between items. A label wider than the label column goes on its own line and the description starts below it, still in the description column. Notes such as [default: 3] and [repeatable] are kept on one line when they fit. A \n in a description starts a new paragraph, and a no-break space (U+00A0) keeps two words together.

Options:
  -f, --force / --no-force  Overwrite existing files in the target directories
                            without asking for confirmation first

      --retries=RETRIES     How many times a failed copy is retried before
                            giving up [default: 3]

  -h, --help                Show help for this command

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.