arielespinoza07 / hephaestus
Forge powerful CLI tools with ease - A modern, type-safe wrapper around Symfony Console
Requires
- php: ^8.5
- psr/container: ^2.0
- symfony/console: ^8.0.6
Requires (Dev)
- laravel/pint: ^1.27.1
- pestphp/pest: ^4.4.1
- phpstan/phpstan: ^2.1.40
README
Build powerful CLI tools with ease
An attribute-driven CLI framework built on Symfony Console. Define commands with PHP attributes โ Hephaestus handles the rest.
Philosophy
Symfony Console is powerful but verbose.
Hephaestus gives you:
- ๐ฏ Simple API for simple tasks
- ๐ Type-safe everywhere (PHP 8.5+)
- โก Fast to write, easy to test
- ๐ง Full Symfony power when needed
- ๐๏ธ Clean architecture โ SOLID, DRY, YAGNI, KISS
- ๐งช Test-friendly design
- ๐ Built-in scaffolding โ generate commands and entrypoints instantly
Requirements
- PHP 8.5+
- Symfony Console 8.0+
- psr/container 2.0+ (pulled in automatically via Composer)
Installation
composer require arielespinoza07/hephaestus
Quick Start
1. Create a command
<?php declare(strict_types=1); use Hephaestus\Console\Command; use Hephaestus\Attributes\Signature; use Hephaestus\Attributes\Description; use Hephaestus\Attributes\Output; use Hephaestus\Attributes\Argument; use Hephaestus\Attributes\Option; #[Signature('app:greet')] #[Description('Greets a user by name')] #[Output] final readonly class GreetCommand extends Command { public function execute( #[Argument(description: 'The name of the user to greet')] string $name, #[Option(description: 'Shout the greeting', shortcut: 'l')] bool $yell = false, ): int { $greeting = sprintf('Hello, %s!', $name); $this->output->writeln($yell ? mb_strtoupper($greeting) : $greeting); return self::SUCCESS; } }
2. Bootstrap your CLI app
<?php declare(strict_types=1); require_once __DIR__ . '/vendor/autoload.php'; use Hephaestus\CliApp; exit( CliApp::create('MyApp', '1.0.0') ->registerCommands(__DIR__ . '/src/Commands') ->run() );
3. Run it
php bin/console app:greet John # Hello, John! php bin/console app:greet John --yell # HELLO, JOHN! php bin/console app:greet John -l # HELLO, JOHN!
Scaffolding
Hephaestus ships a bin/hephaestus binary with two generators to get you started without writing boilerplate.
make:command
Generate a command class skeleton inside your project's src/Commands/ directory (auto-detected from composer.json).
./vendor/bin/hephaestus make:command Greet # โ src/Commands/GreetCommand.php ./vendor/bin/hephaestus make:command Greet --force # overwrite existing ./vendor/bin/hephaestus make:command Greet -f # shortcut
The generated file:
<?php declare(strict_types=1); namespace App\Commands; use Hephaestus\Attributes\Description; use Hephaestus\Attributes\Output; use Hephaestus\Attributes\Signature; use Hephaestus\Console\Command; #[Signature('greet')] #[Description('')] #[Output] final readonly class GreetCommand extends Command { public function execute(): int { return self::SUCCESS; } }
The signature is auto-derived from the class name (CreateUserCommand โ create-user) feel free to change it.
make:entrypoint
Generate an executable CLI entrypoint in bin/.
./vendor/bin/hephaestus make:entrypoint app # โ bin/app (executable, chmod 0755) # Full options ./vendor/bin/hephaestus make:entrypoint app \ --app="My App" \ --ver=2.0.0 \ --dir=src/Commands \ --force
| Option | Default | Description |
|---|---|---|
--app |
ucfirst($name) |
App name passed to CliApp::create() |
--ver |
1.0.0 |
App version |
--dir |
auto-detected from composer.json |
Relative path to commands directory |
--force / -f |
false |
Overwrite if file already exists |
The generated file:
#!/usr/bin/env php <?php declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; use Hephaestus\CliApp; exit( CliApp::create('App', '1.0.0') ->registerCommands(__DIR__ . '/../src/Commands') ->run() );
How It Works
Hephaestus reads PHP attributes on your command class and its execute() method parameters to automatically wire up the Symfony Console command โ no configure(), no $input->getArgument(), no $input->getOption().
PHP Attributes
โ
โผ
MetadataReader โโโบ CommandMetadata
โ
โผ
SymfonyCommandBridge โโโบ Symfony Command
โ
โผ
CLI / CommandTester
Attributes Reference
Class-level attributes
| Attribute | Required | Description |
|---|---|---|
#[Signature('app:name')] |
โ | CLI command name |
#[Description('...')] |
Short description shown in the command list | |
#[Help('...')] |
Extended help shown by help <command> |
|
#[Usage(['app:name arg1', '...'])] |
Example usages shown in help output | |
#[Alias('alias1|alias2')] |
Pipe-separated command aliases | |
#[Input] |
Inject InputInterface into $this->input |
|
#[Output] |
Inject OutputInterface into $this->output |
|
#[Style] |
Inject SymfonyStyle into $this->output |
Parameter-level attributes
#[Argument]
Maps a positional CLI argument (command arg).
#[Argument(
description: 'The user name', // shown in help
required: true, // default: true
default: null, // value when optional and not provided
)]
string $name
Type casting: When the parameter type is
int,float, orbool, the raw CLI string is automatically cast before being passed toexecute().
#[Option]
Maps a named CLI option (command --option or command --option=value).
#[Option(
description: 'Shout the greeting', // shown in help
acceptValue: false, // true: --format=json, false: --verbose flag
default: null, // default value when not provided
shortcut: 'l', // single char or array of chars
)]
bool $yell
Type casting: When the parameter type is
int,float, orbool, the raw CLI string is automatically cast before being passed toexecute().
#[CompositeInput]
Groups multiple arguments/options into a single DTO, keeping execute() clean.
// DTO final readonly class CreateUserInput { public function __construct( #[Argument(description: 'User name')] public string $name, #[Argument(description: 'User email')] public string $email, #[Option(description: 'Grant admin role', shortcut: 'a')] public bool $admin = false, ) {} } // Command public function execute( #[CompositeInput] CreateUserInput $input ): int { // ... }
Command Class
All commands extend Hephaestus\Console\Command and must be final readonly.
abstract readonly class Command { // Available when #[Input] is declared on the class protected ?InputInterface $input; // Available when #[Output] is declared on the class protected ?OutputInterface $output; // Exit code constants protected const int SUCCESS = 0; protected const int FAILURE = 1; protected const int INVALID = 2; }
CliApp
Bootstrap a complete CLI application with auto-discovery of commands.
CliApp::create(string $name, string $version = '1.0.0'): self
// Scan one or multiple directories and register all commands ->registerCommands(string|array $directories, ?string $cachePath = null): self // Examples ->registerCommands(__DIR__ . '/src/Commands') ->registerCommands([__DIR__ . '/src/Commands', __DIR__ . '/src/Plugins']) // With metadata cache for faster startup ->registerCommands(__DIR__ . '/src/Commands', __DIR__ . '/var/cache/commands.cache') // Resolve commands through a PSR-11 container (optional) ->withContainer(ContainerInterface $container): self // Run the CLI application ->run(): int
registerCommands() returns $this for fluent chaining and can be called multiple times for multiple directories.
withContainer() enables dependency injection โ commands are resolved via $container->get() instead of new ClassName().
Testing
Hephaestus ships a first-class testing API built on top of Symfony's CommandTester.
CommandRunner
use Hephaestus\Testing\CommandRunner; $result = CommandRunner::for(GreetCommand::class) ->withArgs(['name' => 'John']) // positional arguments ->withOptions(['yell' => true]) // options (-- prefix added automatically) ->run();
CommandResult assertions
$result->assertSuccessful(); // exit code === 0 $result->assertFailed(); // exit code !== 0 $result->assertExitCode(int $expected); // exact exit code $result->assertOutputContains(string $needle); // stdout contains substring $result->assertOutputEquals(string $expected); // stdout exact match $result->assertErrorOutputContains(string $needle); // stderr contains substring
All assertion methods return $this for fluent chaining:
$result ->assertSuccessful() ->assertExitCode(0) ->assertOutputEquals('Hello, John!');
Inspecting output directly
$result->exitCode(); // int $result->output(); // string (stdout, trimmed) $result->errorOutput(); // string (stderr, trimmed) $result->isSuccessful(); // bool
Full test example (PestPHP)
use Hephaestus\Testing\CommandRunner; test('greets user', function () { CommandRunner::for(GreetCommand::class) ->withArgs(['name' => 'John']) ->run() ->assertSuccessful() ->assertOutputEquals('Hello, John!'); }); test('shouts when yell option is set', function () { CommandRunner::for(GreetCommand::class) ->withArgs(['name' => 'John']) ->withOptions(['yell' => true]) ->run() ->assertOutputEquals('HELLO, JOHN!'); }); test('fails when name argument is missing', function () { CommandRunner::for(GreetCommand::class) ->run() ->assertFailed() ->assertExitCode(1) ->assertErrorOutputContains('missing: "name"'); });
Why "Hephaestus"?
Hephaestus (แผญฯฮฑฮนฯฯฮฟฯ) is the Greek god of fire, metalworking, and the forge โ the divine craftsman who built weapons and tools for gods and heroes alike.
This library follows the same principle: hand developers the right tools so they can forge great things without wasting time on the tedious parts.
๐จ Forge. Build. Ship.
Contributing
See CONTRIBUTING.md for setup instructions, code conventions, and PR guidelines.