Skip to content

Neuron-PHP/cli

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

84 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CI codecov

Neuron CLI Component

A unified command-line interface for the Neuron PHP framework that provides a modern, extensible CLI tool for all Neuron components.

Features

  • 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

Requirements

  • PHP 8.4 or higher
  • Composer

Installation

Local Installation (Recommended)

composer require neuron-php/cli

After installation, the CLI will be available at:

./vendor/bin/neuron

Global Installation

composer global require neuron-php/cli

Make sure your global Composer bin directory is in your PATH, then:

neuron version

Basic Usage

List All Commands

neuron list

Get Help

# General help
neuron --help

# Help for specific command
neuron help make:controller

Config Export

Export 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=always

Notes:

  • Keys become UPPER_SNAKE_CASE using CATEGORY_NAME from YAML.
  • Nested and array values are flattened (e.g., ARRAY_KEY_0, ARRAY_KEY_1).
  • Booleans and nulls are stringified appropriately for env files.

For Component Developers

The CLI component provides two ways for your component to register commands:

Method 1: CLI Provider Class (Recommended)

This is the preferred method as it keeps all command registrations in one place.

  1. Update your component's composer.json:
{
    "name": "neuron-php/your-component",
    "extra": {
        "neuron": {
            "cli-provider": "Neuron\\YourComponent\\Cli\\CommandProvider"
        }
    }
}
  1. 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);
    }
}

Method 2: Direct Registration

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"
            }
        }
    }
}

Creating Command Classes

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
    }
}

Command Naming Conventions

  • 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

Built-in Commands

Core Commands

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

Output Helpers

The Output class provides rich formatting options:

Basic Output

// 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');

Tables

$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 Bars

$progress = $this->output->createProgressBar(100);
$progress->start();

foreach ($items as $item) {
    // Process item...
    $progress->advance();
}

$progress->finish();

Interactive Input

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('> ');

Interactive Input Methods

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('> ')

Example: Interactive Setup Command

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;
    }
}

Secret Input (Passwords)

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;
}

Choice Selection

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)

How Command Discovery Works

  1. Installation Detection: When neuron is run, the ComponentLoader scans for installed packages
  2. Package Filtering: Only neuron-php/* packages are considered
  3. Configuration Check: Each package's composer.json is checked for CLI configuration
  4. Provider Loading: If a CLI provider is found, its register() method is called
  5. Direct Registration: Any directly listed commands are registered
  6. Project Commands: The root project's composer.json is 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

Error Handling

Commands should return appropriate exit codes:

  • 0 - Success
  • 1 - General error
  • 2 - Misuse of command
  • 126 - Command cannot execute
  • 127 - 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;
    }
}

Testing Your Commands

The CLI component provides comprehensive testing support, including the ability to test commands that require user input.

Testing Commands Without 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);
    }
}

Testing Commands With User Input

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);
    }
}

Using Input Reader in Commands

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.

TestInputReader Features

The TestInputReader class provides:

  • Response Queue: Pre-program multiple responses with addResponse() or addResponses()
  • 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 IInputReader methods (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();

Contributing

When adding new features to the CLI component:

  1. Extend the Command base class for new commands
  2. Add tests in the tests/ directory
  3. Update this README with new features
  4. Follow PSR-4 naming conventions
  5. Use the existing code style (tabs, spaces etc)

License

This component is part of the Neuron PHP Framework and is released under the MIT License.

About

Component for writing CLI Applications

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages