hoa/cli
Hoa\Cli is a PHP library for building command-line interfaces, offering abstractions for commands, arguments, options, input/output, and terminal interaction. It fits the modular Hoa ecosystem for structured CLI application development.
hoa/cli is a tiny, archived (last release July 2017) routing layer for command-line input. It is not a full console framework: it matches argv strings against patterns and invokes a callable. There is no option parser, no help generator, no input/output abstraction, and no Laravel integration.
Before installing, check whether you actually need it:
composer require hoa/cli
Expect old version constraints (PHP 5.x-era) and transitive hoa/* dependencies. On a modern Laravel app, install it in a throwaway branch first and run composer why-not hoa/cli / composer update --dry-run to see conflicts.
Where to look first:
src/Router.php — registration and dispatch entry point.src/Router/Route.php — pattern compilation and matching.Hoa\Cli (the API surface is small enough to read in one sitting).The smallest useful script looks roughly like this (verify exact pattern syntax against the source for your pinned version):
<?php
require __DIR__ . '/vendor/autoload.php';
$router = new Hoa\Cli\Router();
$router->add('greet (?<name>\w+)', function ($name) {
echo "Hello, {$name}\n";
});
$router->add('sum (?<a>\d+) (?<b>\d+)', function ($a, $b) {
echo ((int) $a + (int) $b) . "\n";
});
$router->route($_SERVER['argv']);
The realistic first use case in a Laravel codebase is not building a new CLI. It is wrapping an existing legacy Hoa-based script so it can be called from an Artisan command, or maintaining an inherited standalone tool. For anything new, use Illuminate\Console / Symfony Console instead.
Artisan bridge (the only pattern that makes sense inside Laravel). Keep the Hoa router in a standalone script or a small service class, and expose it through one Artisan command:
// app/Console/Commands/LegacyCli.php
protected $signature = 'legacy:cli {args* : Raw arguments passed to the legacy router}';
public function handle()
{
$router = app(\App\Cli\LegacyRouterFactory::class)->make();
// Hoa expects argv-style input; prepend a fake script name.
$argv = array_merge(['legacy'], $this->argument('args'));
ob_start();
$router->route($argv);
$this->output->write(ob_get_clean());
return self::SUCCESS;
}
This keeps argument parsing in Artisan (where --help, validation, and --no-interaction work) and only delegates dispatch to Hoa.
Table-driven route registration. Do not scatter $router->add() calls. Register routes from a config array or a dedicated class so handlers are discoverable and testable:
foreach (config('legacy-cli.routes', []) as $pattern => $handler) {
$router->add($pattern, $handler);
}
Handlers can be invokable classes resolved through the container (fn (...$args) => app($handler)(...$args)), which is the cleanest way to get Laravel services into Hoa callbacks.
Order-sensitive dispatch. Register specific routes before catch-alls. Treat the route list like Laravel route definitions: the first match wins, and a permissive pattern at the top will shadow everything below it.
Explicit fallback and failure handling. Wrap route() in a try/catch for whatever the router throws on no match (inspect the source; it is not a documented contract), then return a non-zero exit code and print usage text you write yourself. Hoa will not do this for you.
Testing. The router is pure PHP and cheap to test. Instantiate it per test, call route() with a synthetic argv, and capture output with ob_start() / ob_get_clean(). Assert on both output and exit behaviour; do not shell out to php artisan for unit-level coverage.
Integration boundary. If you need prompts, progress bars, tables, or coloured output, you are in the wrong package — either keep I/O in the Artisan command or pair with hoa/console, accepting a second archived dependency.
hoa/* packages frequently collide with modern Laravel/Symfony requirements. If Composer refuses, do not force it with --ignore-platform-reqs in a production app; isolate the legacy tool in its own process/package instead.--flag=value handling, no type coercion, no defaults. Anything beyond positional patterns must be parsed by your handler or by Artisan before dispatch.argv includes the script name. Passing raw $_SERVER['argv'] means the first element is the script path. When bridging from Artisan, prepend a dummy element so indices line up.\w+ captures, and remember the shell has already stripped quotes before PHP sees the string — spaces inside a single argument are fine, unquoted spaces split arguments.greet (?<name>\w+) will not match greet Alice (double space). Normalise or document the expected invocation.argv inside the command and log each pattern as it is registered. When a route silently fails to match, the cause is almost always an unescaped character, argument splitting, or an earlier greedy route.$this->app; use app() explicitly or bind invokable handler classes. There are no middleware, events, or lifecycle hooks.Hoa\Cli\Router\Route or swap callables, but there is no plugin system. If you find yourself extending it heavily, that is the signal to replace it with Symfony Console.return self::FAILURE / exit(1) yourself; otherwise CI and shell scripts cannot detect failures.How can I help you explore Laravel packages today?