phalanx / console
CLI application framework for Phalanx
Requires
- php: ^8.4
- phalanx/core: ^1.0
Suggests
- phalanx/parallel: For worker process parallelism in CLI tasks
- phalanx/stream: For streaming data through console commands
This package is auto-updated.
Last update: 2026-03-27 16:55:54 UTC
README
phalanx/console
Build CLI applications with the same scope-driven concurrency that powers Phalanx HTTP servers. Define commands as invokable classes, group them, load them from directories, and let the framework handle argument parsing, validation, and help generation.
Table of Contents
- Installation
- Quick Start
- Defining Commands
- Command Configuration
- Grouping Commands
- Loading Commands from Files
- The CommandScope
- Running Concurrent Work
Installation
composer require phalanx/console
Requires PHP 8.4+ and phalanx/core.
Quick Start
<?php declare(strict_types=1); use Phalanx\Console\CommandScope; use Phalanx\Scope; use Phalanx\Task\Scopeable; final readonly class GreetCommand implements Scopeable { public function __invoke(Scope $scope): mixed { /** @var CommandScope $scope */ $name = $scope->args->get('name', 'world'); echo "Hello, {$name}!\n"; return 0; } }
<?php use Phalanx\Console\Command; use Phalanx\Console\CommandGroup; use Phalanx\Console\ConsoleRunner; $app = Application::starting()->compile(); $commands = CommandGroup::of([ 'greet' => new Command( fn: new GreetCommand(), config: static fn($c) => $c ->withDescription('Greet someone by name') ->withArgument('name', 'Name to greet', required: false), ), ]); $runner = ConsoleRunner::withCommands($app, $commands); exit($runner->run($argv));
$ php app.php greet Jonathan
Hello, Jonathan!
Defining Commands
A Command wraps a handler and a CommandConfig. The handler receives a CommandScope at dispatch time, giving it access to parsed arguments, options, and the full Phalanx execution scope. Use invokable classes for commands with real logic:
<?php declare(strict_types=1); use Phalanx\Console\CommandScope; use Phalanx\Scope; use Phalanx\Task\Scopeable; final readonly class MigrateCommand implements Scopeable { public function __invoke(Scope $scope): mixed { /** @var CommandScope $scope */ $step = $scope->options->get('step', 'all'); $dry = $scope->options->has('dry-run'); $pending = $scope->service(Migrations::class)->pending(); foreach ($pending as $migration) { if ($dry) { echo "[dry-run] Would apply: {$migration->name}\n"; continue; } $scope->execute($migration); echo "Applied: {$migration->name}\n"; } return 0; } }
<?php use Phalanx\Console\Command; $migrate = new Command( fn: new MigrateCommand(), config: static fn($c) => $c ->withDescription('Run pending database migrations') ->withOption('step', 's', 'Number of migrations to run', requiresValue: true) ->withOption('dry-run', 'd', 'Preview without applying'), );
The config parameter accepts either a CommandConfig instance or a closure that receives a fresh CommandConfig and returns the configured version.
Command Configuration
CommandConfig builds up arguments and options through an immutable fluent API:
<?php use Phalanx\Console\CommandConfig; $config = (new CommandConfig()) ->withDescription('Deploy the application') ->withArgument('environment', 'Target environment', required: true) ->withArgument('tag', 'Git tag to deploy', required: false, default: 'latest') ->withOption('force', 'f', 'Skip confirmation prompts') ->withOption('concurrency', 'c', 'Max concurrent tasks', requiresValue: true, default: '4');
Each call returns a new CommandConfig -- the original stays untouched.
Grouping Commands
CommandGroup collects commands into a named registry. Build one from an array, or use the fluent builder:
<?php use Phalanx\Console\CommandGroup; // From an array $commands = CommandGroup::of([ 'deploy' => $deploy, 'migrate' => $migrate, 'seed' => $seed, ]); // Fluent builder $commands = CommandGroup::create() ->command('deploy', $deploy, 'Deploy the application') ->command('migrate', $migrate, 'Run database migrations') ->command('seed', $seed, 'Seed the database'); // Merge groups together $all = $coreCommands->merge($pluginCommands);
Loading Commands from Files
CommandLoader scans a directory and loads every .php file that returns a CommandGroup:
<?php use Phalanx\Console\CommandLoader; // Load all command files from a directory $commands = CommandLoader::loadDirectory(__DIR__ . '/commands');
Each file returns a CommandGroup with invokable handlers:
<?php // commands/deploy.php use Phalanx\Console\Command; use Phalanx\Console\CommandGroup; return CommandGroup::of([ 'deploy' => new Command( fn: new DeployCommand(), config: static fn($c) => $c->withDescription('Deploy the application'), ), ]);
ConsoleRunner accepts directory paths directly and handles the loading:
<?php // Load from one or more directories $runner = ConsoleRunner::withCommands($app, __DIR__ . '/commands'); $runner = ConsoleRunner::withCommands($app, [__DIR__ . '/commands', __DIR__ . '/plugins']);
The CommandScope
CommandScope extends ExecutionScope with typed property hooks for the matched command: $commandName, $args, $options, and $config. Access parsed arguments by position or name, and options by name or shorthand.
Running Concurrent Work
Because CommandScope extends ExecutionScope, every command has access to Phalanx's concurrency primitives. A CLI tool that fetches data from multiple sources concurrently:
<?php declare(strict_types=1); use Phalanx\Console\CommandScope; use Phalanx\ExecutionScope; use Phalanx\Task; use Phalanx\Task\Executable; final readonly class HealthCheckCommand implements Executable { public function __invoke(ExecutionScope $scope): mixed { /** @var CommandScope $scope */ $services = ['api', 'database', 'cache', 'queue']; $results = $scope->concurrent( array_map( static fn(string $name) => Task::of( static fn($s) => $s->service(HealthChecker::class)->check($name) ), $services, ), ); foreach ($services as $i => $name) { $status = $results[$i] ? 'OK' : 'FAIL'; echo " {$name}: {$status}\n"; } return array_any($results, static fn($r) => !$r) ? 1 : 0; } }
<?php use Phalanx\Console\Command; $healthCheck = new Command( fn: new HealthCheckCommand(), config: static fn($c) => $c->withDescription('Check health of all services'), );
All concurrency runs on the ReactPHP event loop under the hood. The command handler reads like synchronous code.