A unified command-line interface for the Neuron PHP framework that provides a modern, extensible CLI tool for all Neuron components.
- Unified CLI Interface - Single entry point for all component commands
- Automatic Command Discovery - Auto-detects commands from installed components
- Rich Terminal Output - Colored output, tables, and progress bars
- Zero Configuration - Works out of the box with any Neuron component
- Extensible Architecture - Easy to add custom commands
- PHP 8.4 or higher
- Composer
composer require neuron-php/cliAfter installation, the CLI will be available at:
./vendor/bin/neuroncomposer global require neuron-php/cliMake sure your global Composer bin directory is in your PATH, then:
neuron versionneuron list# General help
neuron --help
# Help for specific command
neuron help make:controllerExport a YAML config as environment-style pairs for production.
# Export project config to .env production file
./vendor/bin/neuron config:env --config=config/neuron.yaml > .env.production
# Only export selected categories
./vendor/bin/neuron config:env --category=site,cache
# Shell format (export lines)
./vendor/bin/neuron config:env --format=shell
# Quoting control: auto|always|never (default: auto)
./vendor/bin/neuron config:env --quote=alwaysNotes:
- Keys become UPPER_SNAKE_CASE using
CATEGORY_NAMEfrom YAML. - Nested and array values are flattened (e.g.,
ARRAY_KEY_0,ARRAY_KEY_1). - Booleans and nulls are stringified appropriately for env files.
The CLI component provides two ways for your component to register commands:
This is the preferred method as it keeps all command registrations in one place.
- Update your component's
composer.json:
{
"name": "neuron-php/your-component",
"extra": {
"neuron": {
"cli-provider": "Neuron\\YourComponent\\Cli\\CommandProvider"
}
}
}- Create the provider class:
<?phpnamespaceNeuron\YourComponent\Cli;
class CommandProvider
{
publicstaticfunctionregister($app): void
{
// Register your commands$app->register('component:command1', Command1::class);
$app->register('component:command2', Command2::class);
$app->register('component:command3', Command3::class);
}
}For simpler cases, you can register commands directly in composer.json:
{
"name": "neuron-php/your-component",
"extra": {
"neuron": {
"commands": {
"component:command": "Neuron\\YourComponent\\Commands\\YourCommand",
"component:another": "Neuron\\YourComponent\\Commands\\AnotherCommand"
}
}
}
}All commands must extend the base Command class:
<?phpnamespaceNeuron\YourComponent\Commands;
useNeuron\Cli\Commands\Command;
class MakeControllerCommand extends Command
{
/** * Get the command name (how it's invoked) */publicfunctiongetName(): string
{
return'make:controller';
}
/** * Get the command description (shown in list) */publicfunctiongetDescription(): string
{
return'Create a new controller class';
}
/** * Configure arguments and options */publicfunctionconfigure(): void
{
// Add required argument$this->addArgument('name', true, 'The controller name');
// Add optional argument with default$this->addArgument('namespace', false, 'Custom namespace', 'App\\Controllers');
// Add boolean option (flag)$this->addOption('resource', 'r', false, 'Create a resource controller');
// Add option that accepts a value$this->addOption('model', 'm', true, 'Model to bind to controller');
// Add option with default value$this->addOption('template', 't', true, 'Template to use', 'default');
}
/** * Execute the command * * @return int Exit code (0 for success) */publicfunctionexecute(): int
{
// Get arguments$name = $this->input->getArgument('name');
$namespace = $this->input->getArgument('namespace');
// Get options$isResource = $this->input->getOption('resource');
$model = $this->input->getOption('model');
// Output messages$this->output->info("Creating controller: {$name}");
// Show progress for long operations$progress = $this->output->createProgressBar(100);
$progress->start();
for ($i = 0; $i < 100; $i++) {
// Do work...$progress->advance();
usleep(10000);
}
$progress->finish();
// Success message$this->output->success("Controller created successfully!");
return0; // Success
}
}- Use namespace format:
component:action(e.g.,mvc:controller,cms:init) - Use kebab-case for multi-word actions:
make:controller,cache:clear - Group related commands under the same namespace
- Keep names short but descriptive
| Command | Description | Usage |
|---|---|---|
help | Display help for a command | neuron help [command] |
list | List all available commands | neuron list |
version | Show version information | neuron version [--verbose] |
config:env | Export neuron.yaml as KEY=VALUE pairs | `neuron config:env [--config=...] [--category=...] [--format=dotenv |
The Output class provides rich formatting options:
// Simple messages$this->output->write('Simple message');
$this->output->writeln('Message with newline', 'green');
// Styled messages$this->output->info('Information message'); // Cyan$this->output->success('Success message'); // Green with ✓$this->output->warning('Warning message'); // Yellow with ⚠$this->output->error('Error message'); // Red with ✗$this->output->comment('Comment'); // Yellow// Sections and titles$this->output->title('Command Title');
$this->output->section('Section Header');$this->output->table(
['Name', 'Version', 'Description'],
[
['cli', '1.0.0', 'CLI component'],
['mvc', '0.6.0', 'MVC framework'],
['cms', '0.5.0', 'Content management'],
]
);$progress = $this->output->createProgressBar(100);
$progress->start();
foreach ($itemsas$item) {
// Process item...$progress->advance();
}
$progress->finish();The CLI component provides full support for interactive user input in commands:
// Check if terminal is interactiveif (!$this->input->isInteractive()) {
$this->output->error('This command requires an interactive terminal');
return1;
}
// Ask for text input with optional default$name = $this->input->ask('What is your name?', 'Anonymous');
// Ask without default (returns empty string if no input)$email = $this->input->ask('Enter your email');
// Ask yes/no questionsif ($this->input->confirm('Do you want to continue?', true)) {
// User confirmed (pressed y/yes/1/true or Enter with default true)
}
// Read raw input with custom prompt$line = $this->input->readLine('> ');| Method | Description | Example |
|---|---|---|
isInteractive() | Check if terminal supports interaction | if ($this->input->isInteractive()) |
ask($question, $default) | Ask for text input | $name = $this->input->ask('Name?', 'John') |
confirm($question, $default) | Ask yes/no question | if ($this->input->confirm('Continue?', true)) |
askSecret($question) | Ask for hidden input (passwords) | $pass = $this->input->askSecret('Password') |
choice($question, $choices, $default, $multiple) | Select from options | $opt = $this->input->choice('Pick:', ['A', 'B']) |
readLine($prompt) | Read raw input line | $line = $this->input->readLine('> ') |
class SetupCommand extends Command
{
publicfunctionexecute(): int
{
if (!$this->input->isInteractive()) {
$this->output->error('Setup requires interactive mode');
return1;
}
// Gather information$project = $this->input->ask('Project name', 'my-app');
$author = $this->input->ask('Author name');
$email = $this->input->ask('Author email');
// Validate emailwhile (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->output->error('Invalid email format');
$email = $this->input->ask('Author email');
}
// Show summary$this->output->section('Configuration Summary');
$this->output->write("Project: {$project}");
$this->output->write("Author: {$author} <{$email}>");
// Confirmif (!$this->input->confirm('Create project with these settings?', true)) {
$this->output->warning('Setup cancelled');
return1;
}
// Proceed with setup...$this->output->success('Project created successfully!');
return0;
}
}The askSecret() method hides user input, perfect for passwords and sensitive data:
// Ask for password (input will be hidden)$password = $this->input->askSecret('Enter password');
// Confirm password$confirm = $this->input->askSecret('Confirm password');
if ($password !== $confirm) {
$this->output->error('Passwords do not match');
return1;
}The choice() method presents options for selection:
// Simple choice from array$colors = ['Red', 'Green', 'Blue'];
$color = $this->input->choice('Pick a color:', $colors, 'Blue');
// Associative array (key => display value)$environments = [
'dev' => 'Development',
'staging' => 'Staging', 'prod' => 'Production'
];
$env = $this->input->choice('Select environment:', $environments, 'dev');
// Multiple selection$features = [
'api' => 'REST API',
'auth' => 'Authentication',
'cache' => 'Caching'
];
$selected = $this->input->choice(
'Select features to enable:',
$features,
null, // No defaulttrue// Allow multiple
);
// Returns array like: ['api', 'auth']// Users can select by:// - Number: Type "1" for first option// - Key: Type "dev" for development// - Value: Type "Development" (case-insensitive)// - Multiple: "1,3" or "api,cache" (when multiple allowed)- Installation Detection: When
neuronis run, the ComponentLoader scans for installed packages - Package Filtering: Only
neuron-php/*packages are considered - Configuration Check: Each package's
composer.jsonis checked for CLI configuration - Provider Loading: If a CLI provider is found, its
register()method is called - Direct Registration: Any directly listed commands are registered
- Project Commands: The root project's
composer.jsonis also checked
This automatic discovery means:
- No manual registration needed
- Commands available immediately after component installation
- Clean separation between components
- No conflicts between component commands
Commands should return appropriate exit codes:
0- Success1- General error2- Misuse of command126- Command cannot execute127- Command not found
Example:
publicfunctionexecute(): int
{
try {
// Command logic...return0;
} catch (ValidationException$e) {
$this->output->error('Validation failed: ' . $e->getMessage());
return2;
} catch (\Exception$e) {
$this->output->error('An error occurred: ' . $e->getMessage());
return1;
}
}The CLI component provides comprehensive testing support, including the ability to test commands that require user input.
usePHPUnit\Framework\TestCase;
useNeuron\Cli\Console\Input;
useNeuron\Cli\Console\Output;
class MakeControllerCommandTest extends TestCase
{
publicfunctiontestExecute(): void
{
$command = newMakeControllerCommand();
// Mock input$input = newInput(['UserController', '--resource']);
$output = newOutput(false); // No colors for testing$command->setInput($input);
$command->setOutput($output);
$command->configure();
$input->parse($command);
$exitCode = $command->execute();
$this->assertEquals(0, $exitCode);
}
}Commands that use prompt(), confirm(), secret(), or choice() can be tested using the TestInputReader:
usePHPUnit\Framework\TestCase;
useNeuron\Cli\Console\Input;
useNeuron\Cli\Console\Output;
useNeuron\Cli\IO\TestInputReader;
class SetupCommandTest extends TestCase
{
publicfunctiontestInteractiveSetup(): void
{
$command = newSetupCommand();
// Create test input reader with pre-programmed responses$inputReader = newTestInputReader();
$inputReader->addResponse('my-project'); // Project name$inputReader->addResponse('John Doe'); // Author name$inputReader->addResponse('john@example.com'); // Email$inputReader->addResponse('yes'); // Confirmation// Configure command$input = newInput([]);
$output = newOutput(false);
$command->setInput($input);
$command->setOutput($output);
$command->setInputReader($inputReader);
// Execute command$exitCode = $command->execute();
// Assertions$this->assertEquals(0, $exitCode);
// Verify the prompts that were shown$prompts = $inputReader->getPromptHistory();
$this->assertCount(4, $prompts);
$this->assertStringContainsString('Project name', $prompts[0]);
$this->assertStringContainsString('Author name', $prompts[1]);
}
publicfunctiontestUserCancelsSetup(): void
{
$command = newSetupCommand();
// User will cancel the setup$inputReader = newTestInputReader();
$inputReader->addResponses([
'test-project',
'Test User',
'test@example.com',
'no'// Cancel confirmation
]);
$input = newInput([]);
$output = newOutput(false);
$command->setInput($input);
$command->setOutput($output);
$command->setInputReader($inputReader);
$exitCode = $command->execute();
// Should return non-zero exit code when cancelled$this->assertNotEquals(0, $exitCode);
}
}To make your commands testable, use the convenience methods provided by the Command base class:
class SetupCommand extends Command
{
publicfunctionexecute(): int
{
// Use built-in convenience methods instead of reading STDIN directly$name = $this->prompt('Enter project name: ');
if ($this->confirm('Enable caching?', true)) {
// User confirmed
}
$password = $this->secret('Enter password: ');
$env = $this->choice(
'Select environment:',
['development', 'staging', 'production'],
'development'
);
return0;
}
}These convenience methods automatically use the injected IInputReader, making your commands fully testable without requiring actual user input.
The TestInputReader class provides:
- Response Queue: Pre-program multiple responses with
addResponse()oraddResponses() - Prompt History: Track all prompts shown with
getPromptHistory() - Automatic Validation: Throws exceptions if responses run out, helping catch test bugs
- Fluent Interface: Chain
addResponse()calls for cleaner test setup - Full Interface Support: Implements all
IInputReadermethods (prompt, confirm, secret, choice)
$reader = newTestInputReader();
$reader
->addResponse('value1')
->addResponse('value2')
->addResponse('yes');
// Check if there are responses remainingif ($reader->hasMoreResponses()) {
$count = $reader->getRemainingResponseCount();
}
// Get history of prompts$prompts = $reader->getPromptHistory();
// Reset for reuse$reader->reset();When adding new features to the CLI component:
- Extend the
Commandbase class for new commands - Add tests in the
tests/directory - Update this README with new features
- Follow PSR-4 naming conventions
- Use the existing code style (tabs, spaces etc)
This component is part of the Neuron PHP Framework and is released under the MIT License.