A standalone PHP CLI framework built from scratch

Installs: 7

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 6

pkg:composer/marwen-brini/yalla


README

Tests Code Coverage PHP Version Latest Version Documentation

A standalone PHP CLI framework built from scratch without dependencies.

📚 Read the full documentation

Features

  • Zero Dependencies: Built entirely from scratch without relying on Symfony Console or other frameworks
  • Interactive REPL: Full-featured Read-Eval-Print-Loop for interactive PHP development
  • Command Routing: Custom command parser and router
  • Colored Output: ANSI color support for beautiful terminal output (cross-platform)
  • Advanced Table Rendering: Professional table formatter with multiple border styles, emoji support, and alignment options
  • Migration Tables: Specialized table formatter for database migration systems
  • Progress Indicators: Progress bars, spinners, and step indicators for long-running tasks
  • Async Command Execution (v2.0): Run commands asynchronously with promises and parallel execution
  • Signal Handling (v2.0): Graceful shutdown and cleanup on interrupt signals (Unix/Linux)
  • Command Middleware (v2.0): Authentication, logging, timing, and custom middleware pipeline
  • Dry Run Mode (v2.0): Preview command operations without executing them
  • Environment Management (v2.0): .env file support with variable expansion
  • File System Helpers (v2.0): Safe file operations, backup, unique filenames, and directory utilities
  • Stub Generator (v2.0): Template-based code generation with conditionals and loops
  • Process Locking (v2.0): Prevent concurrent command execution with file-based locks
  • Command Aliases (v2.0): Create shortcuts for frequently used commands
  • Exit Codes (v2.0): Standard exit codes with descriptions and exception mapping
  • Command Signatures (v2.0): Laravel-style signature parsing for arguments and options
  • Enhanced Output (v2.0): Output sections, semantic methods, verbosity levels, and timestamped logs
  • Input Parsing: Handles commands, arguments, and options (long and short formats)
  • Command Scaffolding: Built-in create:command to generate new command boilerplate
  • History & Autocomplete: REPL with command history and intelligent autocompletion
  • Extensible Architecture: Plugin system for custom REPL extensions
  • 100% Test Coverage: Fully tested with Pest PHP
  • Cross-Platform: Works on Windows, macOS, and Linux

Requirements

  • PHP 8.1, 8.2, 8.3, or 8.4
  • Composer 2.0+

Installation

composer require marwen-brini/yalla

Usage

Basic Usage

Create a CLI script (e.g., bin/yalla):

#!/usr/bin/env php
<?php

require 'vendor/autoload.php';

use Yalla\Application;

$app = new Application('Yalla CLI', '2.0.0');
$app->run();

Make it executable:

chmod +x bin/yalla

Creating Custom Commands

<?php

use Yalla\Commands\Command;
use Yalla\Output\Output;

class GreetCommand extends Command
{
    public function __construct()
    {
        $this->name = 'greet';
        $this->description = 'Greet someone';
        
        $this->addArgument('name', 'The name to greet', true);
        $this->addOption('yell', 'y', 'Yell the greeting', false);
    }
    
    public function execute(array $input, Output $output): int
    {
        $name = $this->getArgument($input, 'name');
        $message = "Hello, $name!";
        
        if ($this->getOption($input, 'yell')) {
            $message = strtoupper($message);
        }
        
        $output->success($message);
        
        return 0;
    }
}

// Register in your application
$app = new Application('Yalla CLI', '2.0.0');
$app->register(new GreetCommand());
$app->run();

Running Commands

# List all commands
./bin/yalla list

# Get help for a command
./bin/yalla help greet

# Run a command
./bin/yalla greet World
./bin/yalla greet World --yell
./bin/yalla greet World -y

Command Scaffolding

Yalla includes a built-in command generator to quickly create new commands:

# Create a new command with default settings
./bin/yalla create:command deploy

# Create with custom class name
./bin/yalla create:command deploy --class=DeployApplicationCommand

# Create in a custom directory
./bin/yalla create:command deploy --dir=src/Commands/Deployment

# Force overwrite if file exists
./bin/yalla create:command deploy --force

This will generate a command class with the proper structure:

<?php

declare(strict_types=1);

namespace Yalla\Commands;

use Yalla\Commands\Command;
use Yalla\Output\Output;

class DeployCommand extends Command
{
    public function __construct()
    {
        $this->name = 'deploy';
        $this->description = 'Description of your command';
        
        // Define arguments and options here
    }
    
    public function execute(array $input, Output $output): int
    {
        $output->info('Executing deploy command...');
        
        // Your command logic here
        
        $output->success('deploy completed successfully!');
        
        return 0;
    }
}

Interactive REPL

Yalla includes a powerful REPL (Read-Eval-Print-Loop) for interactive PHP development:

Starting the REPL

# Start the REPL
./bin/yalla repl

# With custom configuration
./bin/yalla repl --config=repl.config.php

# Disable colors
./bin/yalla repl --no-colors

# Disable history
./bin/yalla repl --no-history

REPL Commands

  • :help - Show available commands
  • :exit - Exit the REPL
  • :vars - Show defined variables
  • :imports - Show imported classes
  • :clear - Clear the screen
  • :history - Show command history
  • :mode [mode] - Switch display mode (compact, verbose, json, dump)

REPL Features

  • Command History: Navigate through previous commands with up/down arrows
  • Variable Persistence: Variables defined in the REPL persist across commands
  • Shortcuts: Define shortcuts for frequently used classes
  • Auto-imports: Automatically import specified classes
  • Custom Extensions: Add your own commands and functionality
  • Multiple Display Modes: Choose between compact, verbose, JSON, or dump output formats
  • Smart Object Display: Enhanced display for objects with public properties or __toString() methods
  • Semicolon Support: Natural PHP syntax with trailing semicolons
  • ORM-Friendly: Properly handles objects with protected/private properties

Display Modes

The REPL supports multiple display modes for different use cases:

# Compact mode (default) - Clean, colorized output
[1] > ['id' => 1, 'name' => 'Alice']
['id' => 1, 'name' => "Alice"]

# Verbose mode - Detailed object/array information
[2] > :mode verbose
[3] > $user
═══ Object Details ═══
Class: User
Properties:
  public $id = 1
  public $name = "Alice"
Public Methods:
  - save($params)
  - delete()

# JSON mode - Perfect for API data
[4] > :mode json
[5] > ['status' => 'success', 'data' => ['id' => 1]]
{
  "status": "success",
  "data": {"id": 1}
}

# Dump mode - Traditional PHP debugging
[6] > :mode dump
[7] > "test"
string(4) "test"

REPL Configuration

Create a repl.config.php file:

<?php

return [
    'shortcuts' => [
        'User' => '\App\Models\User',
        'DB' => '\Illuminate\Support\Facades\DB',
    ],
    
    'imports' => [
        ['class' => '\Carbon\Carbon', 'alias' => 'Carbon'],
    ],
    
    'display' => [
        'prompt' => 'myapp> ',
        'performance' => true, // Show execution time and memory
        'mode' => 'compact',   // Options: compact, verbose, json, dump
    ],
    
    'history' => [
        'file' => $_ENV['HOME'] . '/.myapp_history',
        'max_entries' => 1000,
    ],
];

Creating REPL Extensions

use Yalla\Repl\ReplExtension;
use Yalla\Repl\ReplContext;

class MyExtension implements ReplExtension
{
    public function register(ReplContext $context): void
    {
        // Add custom commands
        $context->addCommand('models', function($args, $output) {
            // List all models
        });
        
        // Add shortcuts
        $context->addShortcut('User', '\App\Models\User');
        
        // Add custom formatters
        $context->addFormatter('MyClass', function($value, $output) {
            $output->info('Custom formatting for MyClass');
        });
    }
    
    public function boot(): void
    {
        // Bootstrap your extension
    }
    
    public function getName(): string
    {
        return 'My Custom Extension';
    }
    
    public function getVersion(): string
    {
        return '1.4.0';
    }
    
    public function getDescription(): string
    {
        return 'Adds custom functionality to the REPL';
    }
}

Creating Commands

Commands extend the base Command class:

use Yalla\Commands\Command;
use Yalla\Output\Output;

class MyCommand extends Command
{
    public function __construct()
    {
        $this->name = 'my:command';
        $this->description = 'Does something awesome';
        
        // Add arguments
        $this->addArgument('input', 'Input file', true);
        $this->addArgument('output', 'Output file', false);
        
        // Add options
        $this->addOption('force', 'f', 'Force overwrite', false);
        $this->addOption('verbose', 'v', 'Verbose output', false);
    }
    
    public function execute(array $input, Output $output): int
    {
        // Get arguments
        $inputFile = $this->getArgument($input, 'input');
        $outputFile = $this->getArgument($input, 'output', 'output.txt');
        
        // Get options
        $force = $this->getOption($input, 'force', false);
        $verbose = $this->getOption($input, 'verbose', false);
        
        // Use output methods
        $output->info('Processing...');
        $output->success('Done!');
        $output->error('Something went wrong!');
        $output->warning('Be careful!');
        
        // Draw tables with enhanced formatting
        $output->table(
            ['Name', 'Age', 'City', 'Status'],
            [
                ['John', '30', 'New York', '✅ Active'],
                ['Jane', '25', 'London', '⏳ Pending'],
                ['Bob', '35', 'Paris', '❌ Inactive'],
            ],
            [
                'borders' => 'unicode',
                'alignment' => ['left', 'center', 'left', 'center']
            ]
        );
        
        return 0; // Success
    }
}

Output Formatting

The Output class provides various formatting methods:

// Basic output
$output->write('Hello');           // No newline
$output->writeln('Hello');         // With newline

// Colored output
$output->success('Success!');      // Green
$output->error('Error!');          // Red
$output->warning('Warning!');      // Yellow
$output->info('Info');             // Cyan

// Custom colors
$output->writeln($output->color('Custom', Output::MAGENTA));

// Basic tables
$output->table(['Header 1', 'Header 2'], $rows);

Advanced Table Formatting (New in v1.4)

Yalla 1.4 introduces a powerful table formatter with multiple border styles, emoji support, and advanced formatting options:

use Yalla\Output\Table;

// Create table with options
$table = $output->createTable([
    'borders' => Table::BORDER_UNICODE,  // unicode, ascii, markdown, etc.
    'alignment' => [Table::ALIGN_LEFT, Table::ALIGN_CENTER, Table::ALIGN_RIGHT],
    'colors' => true,
    'max_width' => 120
]);

$table->setHeaders(['Migration', 'Batch', 'Status', 'Date'])
      ->addRow(['2024_01_create_users', '1', '✅ Migrated', '2024-01-15'])
      ->addRow(['2024_02_create_posts', '2', '⏳ Pending', null])
      ->addRow(['2024_03_add_indexes', '3', '❌ Error', null])
      ->render();

Border Styles

// Available border styles
Table::BORDER_UNICODE   // ┌─┬─┐ (default)
Table::BORDER_ASCII     // +---+
Table::BORDER_MARKDOWN  // | --- |
Table::BORDER_DOUBLE    // ╔═╦═╗
Table::BORDER_ROUNDED   // ╭─┬─╮
Table::BORDER_COMPACT   // minimal
Table::BORDER_NONE      // no borders

Advanced Features

// Column alignment
$table->setOptions(['alignment' => [
    Table::ALIGN_LEFT,    // Left align
    Table::ALIGN_CENTER,  // Center align
    Table::ALIGN_RIGHT    // Right align
]]);

// Cell formatters
$table->setCellFormatter(2, function($value) {
    return is_numeric($value) ? number_format($value) : $value;
});

// Sorting and filtering
$table->sortBy(1, 'desc')        // Sort by column 1 descending
      ->filter(fn($row) => $row[2] === 'Active'); // Filter rows

// Row indices
$table->setOptions(['show_index' => true, 'index_name' => 'ID']);

Migration Tables

For database migration systems, use the specialized MigrationTable:

use Yalla\Output\MigrationTable;

$migrationTable = new MigrationTable($output);
$migrationTable->addMigration('2024_01_create_users', 1, 'migrated', '2024-01-15')
               ->addMigration('2024_02_create_posts', 2, 'pending', null)
               ->addMigration('2024_03_add_indexes', null, 'error: Constraint violation', null)
               ->render();

// Render summary
$migrationTable->renderSummary();

Progress Indicators (New in v1.5)

Yalla provides powerful progress indicators for long-running tasks:

Progress Bars

use Yalla\Output\Output;

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

for ($i = 0; $i < 100; $i++) {
    // Do work...
    $progress->advance();
}

$progress->finish();

Multiple formats available:

  • normal: Basic progress bar with percentage
  • verbose: Includes custom messages
  • detailed: Shows time estimates
  • minimal: Compact display
  • memory: Displays memory usage

Spinners

$spinner = $output->createSpinner('Processing...', 'dots');
$spinner->start();

// Do work...
for ($i = 0; $i < 30; $i++) {
    usleep(100000);
    $spinner->advance();
}

$spinner->success('Process complete!');
// Also available: ->error(), ->warning(), ->info()

Frame styles: dots, line, pipe, arrow, bounce, box

Step Indicators

$steps = $output->steps([
    'Download files',
    'Install dependencies',
    'Run migrations',
    'Clear cache'
]);

$steps->start();

// Mark steps as you complete them
$steps->complete(0, 'Files downloaded');
$steps->complete(1, 'Dependencies installed');
$steps->skip(2, 'Migrations skipped');
$steps->fail(3, 'Cache clear failed');

$steps->finish();

New in v2.0: Advanced Features

Async Command Execution

Run commands asynchronously with promises and parallel execution:

use Yalla\Commands\Traits\SupportsAsync;

class ProcessCommand extends Command
{
    use SupportsAsync;

    protected bool $runAsync = true; // Enable async by default

    public function execute(array $input, Output $output): int
    {
        // Long running task...
        $output->info('Processing data...');
        sleep(5);
        return 0;
    }
}

// Run multiple commands in parallel
$command->runParallel([
    fn() => $this->processFile('file1.txt'),
    fn() => $this->processFile('file2.txt'),
    fn() => $this->processFile('file3.txt'),
], $output);

Signal Handling

Gracefully handle interrupt signals (Unix/Linux):

use Yalla\Commands\Traits\HandlesSignals;

class LongRunningCommand extends Command
{
    use HandlesSignals;

    public function execute(array $input, Output $output): int
    {
        // Register signal handlers
        $this->onInterrupt(function() use ($output) {
            $output->warning('Received interrupt signal, cleaning up...');
            $this->cleanup();
            exit(130);
        });

        $this->onTerminate(function() use ($output) {
            $output->warning('Received terminate signal...');
            $this->cleanup();
            exit(143);
        });

        // Process work...
        while ($this->hasWork()) {
            $this->dispatchSignals();
            $this->processNextItem();
        }

        return 0;
    }
}

Command Middleware

Add authentication, logging, timing, and custom middleware:

use Yalla\Commands\Traits\HasMiddleware;
use Yalla\Commands\Middleware\TimingMiddleware;
use Yalla\Commands\Middleware\LoggingMiddleware;

class DeployCommand extends Command
{
    use HasMiddleware;

    public function __construct()
    {
        parent::__construct();

        $this->middleware(new TimingMiddleware())
             ->middleware(new LoggingMiddleware())
             ->middleware(new AuthenticationMiddleware());
    }
}

// Create custom middleware
class AuthenticationMiddleware implements MiddlewareInterface
{
    public function handle(Command $command, array $input, Output $output, callable $next): int
    {
        if (!$this->isAuthenticated()) {
            $output->error('Authentication required');
            return 1;
        }

        return $next($command, $input, $output);
    }

    public function getPriority(): int
    {
        return 100; // Higher priority runs first
    }
}

Dry Run Mode

Preview operations without executing them:

use Yalla\Commands\Traits\DryRunnable;

class MigrationCommand extends Command
{
    use DryRunnable;

    public function execute(array $input, Output $output): int
    {
        $this->setDryRun($this->getOption($input, 'dry-run', false));
        $this->setDryRunOutput($output);

        $this->executeOrSimulate(
            'Create users table',
            fn() => $this->createUsersTable(),
            ['table' => 'users', 'columns' => 5]
        );

        $this->executeOrSimulate(
            'Add indexes',
            fn() => $this->addIndexes()
        );

        if ($this->isDryRun()) {
            $this->showDryRunSummary();
        }

        return 0;
    }
}

// Run with --dry-run flag
// ./bin/yalla migrate --dry-run

Environment Management

Load and manage environment variables with .env support:

use Yalla\Environment\Environment;

$env = new Environment(['.env', '.env.local']);

// Get values with type casting
$dbHost = $env->get('DB_HOST', 'localhost');
$dbPort = $env->getInt('DB_PORT', 3306);
$debug = $env->getBool('APP_DEBUG', false);
$allowedHosts = $env->getArray('ALLOWED_HOSTS'); // Comma-separated

// Environment detection
if ($env->isProduction()) {
    // Production logic
}

if ($env->isDebug()) {
    $output->info('Debug mode enabled');
}

// Variable expansion
// APP_URL=https://example.com
// API_URL=${APP_URL}/api
$apiUrl = $env->get('API_URL'); // https://example.com/api

File System Helpers

Safe file operations with backup and atomic writes:

use Yalla\Filesystem\FileHelper;

$helper = new FileHelper();

// Safe write with automatic backup
$helper->safeWrite('/path/to/config.php', $content, backup: true);

// Generate unique filenames
$logFile = $helper->uniqueFilename('/logs', 'app-{date}-{counter}.log');
// Result: /logs/app-2025-10-01-1.log

// Copy directories recursively
$helper->copyDirectory('/source', '/destination', overwrite: false);

// Find files with pattern
$phpFiles = $helper->findFiles('/src', '*.php', recursive: true);

// Human-readable file sizes
echo $helper->humanFilesize('/large/file.zip'); // "1.5 GB"

// Relative paths
$rel = $helper->relativePath('/var/www/app', '/var/www/app/public');
// Result: "public"

Stub Generator

Template-based code generation with conditionals and loops:

use Yalla\Filesystem\StubGenerator;

$generator = new StubGenerator();
$generator->registerStubDirectory(__DIR__ . '/stubs');

// Generate from template
$generator->generate('model', '/app/Models/User.php', [
    'namespace' => 'App\\Models',
    'class' => 'User',
    'table' => 'users',
    'fillable' => ['name', 'email', 'password'],
    'timestamps' => true,
    'softDeletes' => false,
]);

// Template: model.stub
// namespace {{ namespace }};
//
// class {{ class }}
// {
//     @if(timestamps)
//     protected $timestamps = true;
//     @endif
//
//     protected $fillable = [
//         @each(fillable as item)
//         '{{ item }}',
//         @endeach
//     ];
// }

Process Locking

Prevent concurrent command execution:

use Yalla\Process\LockManager;

class CronCommand extends Command
{
    public function execute(array $input, Output $output): int
    {
        $lockManager = new LockManager();

        if (!$lockManager->acquire('cron-job', timeout: 60)) {
            $output->error('Another instance is running');
            return 1;
        }

        try {
            // Do work...
            $this->processCronJobs();

            // Keep lock alive during long operations
            $lockManager->refresh('cron-job');

        } finally {
            $lockManager->release('cron-job');
        }

        return 0;
    }
}

// Check lock status
$status = $lockManager->getLockStatus('cron-job');
// Check if stale
if ($lockManager->isStale('cron-job', maxAge: 3600)) {
    $lockManager->forceRelease('cron-job');
}

Command Aliases

Create shortcuts for frequently used commands:

$app = new Application('Yalla CLI', '2.0.0');

// Register command with aliases
$migrateCommand = new MigrateCommand();
$migrateCommand->setAliases(['m', 'db:migrate']);

$app->register($migrateCommand);

// Or use fluent API
$app->alias('deploy', 'd');
$app->alias('deploy', 'dep');

// All these work the same:
// ./bin/yalla migrate
// ./bin/yalla m
// ./bin/yalla db:migrate

Exit Codes

Standard exit codes with descriptions and exception mapping:

use Yalla\Commands\ExitCodes;

class BackupCommand extends Command implements ExitCodes
{
    public function execute(array $input, Output $output): int
    {
        if (!$this->canConnect()) {
            return $this->returnWithCode(
                self::EXIT_UNAVAILABLE,
                'Database unavailable',
                $output
            );
        }

        try {
            $this->createBackup();
            return $this->returnSuccess($output);
        } catch (\Exception $e) {
            return $this->handleException($e, $output, debug: true);
        }
    }
}

// Available exit codes:
// EXIT_SUCCESS = 0
// EXIT_ERROR = 1
// EXIT_INVALID_ARGUMENT = 2
// EXIT_UNAVAILABLE = 69
// EXIT_PERMISSION_DENIED = 77
// EXIT_CONFIG_ERROR = 78

Command Signatures

Laravel-style signature parsing:

use Yalla\Commands\Traits\HasSignature;

class DeployCommand extends Command
{
    use HasSignature;

    protected string $signature = 'deploy {environment} {--force} {--tag=latest}';

    public function execute(array $input, Output $output): int
    {
        $environment = $this->argument('environment');
        $force = $this->option('force');
        $tag = $this->option('tag');

        $output->info("Deploying to $environment with tag $tag");

        return 0;
    }
}

Enhanced Output

Output sections, semantic methods, and verbosity levels:

// Output sections for dynamic updates
$section1 = $output->section('Downloads');
$section2 = $output->section('Processing');

$section1->writeln('Downloading file 1...');
$section2->writeln('Processing data...');

// Update sections dynamically
$section1->overwrite('Download complete!');

// Semantic output methods
$output->success('✓ Operation completed');
$output->error('✗ Operation failed');
$output->warning('⚠ Warning message');
$output->info('ℹ Information');

// Verbosity levels
$output->verbose('Detailed information', 'v');
$output->debug('Debug information', 'vvv');

// Timestamped output
$output->withTimestamps(true);
$output->info('This has a timestamp');

// SQL query logging
$output->logQuery('SELECT * FROM users WHERE id = ?', [1], 0.023);

// Grouped output
$output->startGroup('Database Operations');
$output->info('Creating tables...');
$output->info('Adding indexes...');
$output->endGroup();

## Architecture

Yalla is built with a clean, modular architecture:

- **Application**: Main entry point that handles command registration and execution
- **CommandRegistry**: Manages command registration and retrieval
- **InputParser**: Parses CLI arguments into structured data
- **Output**: Handles all terminal output with color support
- **Command**: Base class for all commands

## Testing

Yalla maintains 100% code coverage with comprehensive test suite using Pest PHP:

```bash
# Run tests
composer test

# Run tests with coverage (requires Xdebug or PCOV)
composer test-coverage

# Generate HTML coverage report
composer test-coverage-html

# Run specific test file
vendor/bin/pest tests/ApplicationTest.php

Documentation

Full documentation is available at https://marwen-brini.github.io/Yalla/

Quick Links

Contributing

We welcome contributions! Please see our Contributing Guide for details.

Quick Start

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Author

Marwen-Brini - Initial work - GitHub

Acknowledgments

  • Thanks to all contributors who have helped shape Yalla CLI
  • Inspired by the simplicity and power of modern CLI tools
  • Built with love for the PHP community