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:
<?php
namespace Neuron\YourComponent\Cli;
class CommandProvider
{
public static function register($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:
<?php
namespace Neuron\YourComponent\Commands;
use Neuron\Cli\Commands\Command;
class MakeControllerCommand extends Command
{
/**
* Get the command name (how it's invoked)
*/
public function getName(): string
{
return 'make:controller';
}
/**
* Get the command description (shown in list)
*/
public function getDescription(): string
{
return 'Create a new controller class';
}
/**
* Configure arguments and options
*/
public function configure(): 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)
*/
public function execute(): 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!");
return 0; // 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 ($items as $item) {
// Process item...
$progress->advance();
}
$progress->finish();The CLI component provides full support for interactive user input in commands:
// Check if terminal is interactive
if (!$this->input->isInteractive()) {
$this->output->error('This command requires an interactive terminal');
return 1;
}
// 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 questions
if ($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
{
public function execute(): int
{
if (!$this->input->isInteractive()) {
$this->output->error('Setup requires interactive mode');
return 1;
}
// Gather information
$project = $this->input->ask('Project name', 'my-app');
$author = $this->input->ask('Author name');
$email = $this->input->ask('Author email');
// Validate email
while (!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}>");
// Confirm
if (!$this->input->confirm('Create project with these settings?', true)) {
$this->output->warning('Setup cancelled');
return 1;
}
// Proceed with setup...
$this->output->success('Project created successfully!');
return 0;
}
}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');
return 1;
}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 default
true // 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:
public function execute(): int
{
try {
// Command logic...
return 0;
} catch (ValidationException $e) {
$this->output->error('Validation failed: ' . $e->getMessage());
return 2;
} catch (\Exception $e) {
$this->output->error('An error occurred: ' . $e->getMessage());
return 1;
}
}The CLI component provides comprehensive testing support, including the ability to test commands that require user input.
use PHPUnit\Framework\TestCase;
use Neuron\Cli\Console\Input;
use Neuron\Cli\Console\Output;
class MakeControllerCommandTest extends TestCase
{
public function testExecute(): void
{
$command = new MakeControllerCommand();
// Mock input
$input = new Input(['UserController', '--resource']);
$output = new Output(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:
use PHPUnit\Framework\TestCase;
use Neuron\Cli\Console\Input;
use Neuron\Cli\Console\Output;
use Neuron\Cli\IO\TestInputReader;
class SetupCommandTest extends TestCase
{
public function testInteractiveSetup(): void
{
$command = new SetupCommand();
// Create test input reader with pre-programmed responses
$inputReader = new TestInputReader();
$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 = new Input([]);
$output = new Output(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]);
}
public function testUserCancelsSetup(): void
{
$command = new SetupCommand();
// User will cancel the setup
$inputReader = new TestInputReader();
$inputReader->addResponses([
'test-project',
'Test User',
'test@example.com',
'no' // Cancel confirmation
]);
$input = new Input([]);
$output = new Output(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
{
public function execute(): 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'
);
return 0;
}
}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 = new TestInputReader();
$reader
->addResponse('value1')
->addResponse('value2')
->addResponse('yes');
// Check if there are responses remaining
if ($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.