Silly

A micro-framework for creating CLI applications in PHP. Based on Symfony Console, provides a simpler API.

phpclicommand-linemicro-framework

Framework

Silly

Overview

Silly is a micro-framework for creating CLI applications in PHP. Based on Symfony Console, it provides a simpler API. It's chosen by developers who want to quickly create simple CLI tools and can be started with minimal configuration as a lightweight framework.

Details

Silly leverages the power of Symfony Console component while providing a more intuitive and concise API. It allows you to define commands with a functional approach without complex configuration, making it ideal for developing small CLI tools.

Key Features

  • Simple API: Define commands with minimal code
  • Functional Approach: Write command logic with closures or functions
  • Symfony Console Foundation: Leverage stable Symfony Console features
  • Lightweight: Minimal dependencies and overhead
  • Flexibility: Direct access to Symfony Console features when needed
  • PSR Compliant: Integration with PSR-11 containers
  • CLI Display: Rich display features like progress bars, tables

Pros and Cons

Pros

  • Low Learning Curve: Very simple and easy-to-understand API
  • Rapid Development: Build CLI tools quickly with minimal code
  • Lightweight: Few dependencies and low memory usage
  • Symfony Compatible: Can utilize rich Symfony Console features
  • Flexibility: Supports both functional and object-oriented approaches

Cons

  • Feature Limitations: Not suitable for large, complex CLI applications
  • Community: Smaller community compared to other major frameworks
  • Extensibility: Limited advanced customization capabilities
  • Documentation: Relatively fewer documents and resources

Key Links

Usage Examples

Installation and Basic Setup

composer require silly/silly

Basic Usage

#!/usr/bin/env php
<?php
// cli-app.php

require_once 'vendor/autoload.php';

use Silly\Application;

$app = new Application();

// Simple command definition
$app->command('hello [name]', function ($name, $output) {
    $name = $name ?: 'World';
    $output->writeln("Hello $name!");
});

// Command with options
$app->command('greet name [--yell]', function ($name, $yell, $output) {
    $message = "Hello $name";
    
    if ($yell) {
        $message = strtoupper($message);
    }
    
    $output->writeln($message);
});

// Command with multiple arguments
$app->command('sum numbers*', function (array $numbers, $output) {
    $sum = array_sum($numbers);
    $output->writeln("Sum: $sum");
});

$app->run();

Advanced Example

#!/usr/bin/env php
<?php
// advanced-cli.php

require_once 'vendor/autoload.php';

use Silly\Application;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

$app = new Application('File Manager', '1.0.0');

// File processing command
$app->command('process:files input [--format=] [--output=]', function (
    $input, 
    $format, 
    $output, 
    InputInterface $inputInterface, 
    OutputInterface $outputInterface
) {
    $format = $format ?: 'txt';
    
    $outputInterface->writeln("<info>Processing files from: $input</info>");
    $outputInterface->writeln("<comment>Format: $format</comment>");
    
    // Get file list
    $files = glob("$input/*.$format");
    
    if (empty($files)) {
        $outputInterface->writeln("<error>No files found with format: $format</error>");
        return 1;
    }
    
    // Show progress bar
    $progressBar = new ProgressBar($outputInterface, count($files));
    $progressBar->start();
    
    $results = [];
    foreach ($files as $file) {
        $results[] = [
            'file' => basename($file),
            'size' => filesize($file),
            'modified' => date('Y-m-d H:i:s', filemtime($file))
        ];
        
        $progressBar->advance();
        usleep(100000); // Simulate processing
    }
    
    $progressBar->finish();
    $outputInterface->writeln('');
    
    // Display results in table
    $table = new Table($outputInterface);
    $table->setHeaders(['File', 'Size (bytes)', 'Modified']);
    $table->setRows($results);
    $table->render();
    
    // File output
    if ($output) {
        $csv = "File,Size,Modified\n";
        foreach ($results as $row) {
            $csv .= implode(',', $row) . "\n";
        }
        file_put_contents($output, $csv);
        $outputInterface->writeln("<info>Results saved to: $output</info>");
    }
    
    return 0;
});

// Utility command
$app->command('utils:hash file [--algorithm=]', function ($file, $algorithm, $output) {
    $algorithm = $algorithm ?: 'md5';
    
    if (!file_exists($file)) {
        $output->writeln("<error>File not found: $file</error>");
        return 1;
    }
    
    $supportedAlgorithms = ['md5', 'sha1', 'sha256'];
    if (!in_array($algorithm, $supportedAlgorithms)) {
        $output->writeln("<error>Unsupported algorithm. Use: " . implode(', ', $supportedAlgorithms) . "</error>");
        return 1;
    }
    
    $hash = hash_file($algorithm, $file);
    
    $output->writeln("<info>File:</info> $file");
    $output->writeln("<info>Algorithm:</info> $algorithm");
    $output->writeln("<info>Hash:</info> $hash");
    
    return 0;
});

$app->run();

DI Container Integration

#!/usr/bin/env php
<?php
// di-example.php

require_once 'vendor/autoload.php';

use Silly\Application;
use Psr\Container\ContainerInterface;

// Simple DI container implementation
class SimpleContainer implements ContainerInterface
{
    private array $services = [];
    
    public function set(string $id, $service): void
    {
        $this->services[$id] = $service;
    }
    
    public function get(string $id)
    {
        if (!$this->has($id)) {
            throw new \Exception("Service not found: $id");
        }
        
        $service = $this->services[$id];
        
        if (is_callable($service)) {
            return $service($this);
        }
        
        return $service;
    }
    
    public function has(string $id): bool
    {
        return isset($this->services[$id]);
    }
}

// Service definitions
class DatabaseService
{
    public function connect(): string
    {
        return "Connected to database";
    }
    
    public function query(string $sql): array
    {
        return ["Result for: $sql"];
    }
}

class LoggerService  
{
    public function log(string $message): void
    {
        echo "[" . date('Y-m-d H:i:s') . "] $message\n";
    }
}

// Container setup
$container = new SimpleContainer();
$container->set('database', function () {
    return new DatabaseService();
});
$container->set('logger', function () {
    return new LoggerService();
});

// Create application
$app = new Application('DI Example', '1.0.0');
$app->useContainer($container);

// Commands using services
$app->command('db:status', function (DatabaseService $database, LoggerService $logger, $output) {
    $logger->log('Checking database status...');
    $status = $database->connect();
    $output->writeln("<info>$status</info>");
    $logger->log('Database status check completed');
});

$app->run();

Testing Example

<?php
// tests/CliTest.php

use PHPUnit\Framework\TestCase;
use Silly\Application;
use Symfony\Component\Console\Tester\ApplicationTester;

class CliTest extends TestCase
{
    private Application $app;
    private ApplicationTester $tester;
    
    protected function setUp(): void
    {
        $this->app = new Application();
        
        // Add test command
        $this->app->command('test:hello [name]', function ($name, $output) {
            $name = $name ?: 'World';
            $output->writeln("Hello $name!");
        });
        
        $this->tester = new ApplicationTester($this->app);
    }
    
    public function testHelloCommand(): void
    {
        $this->tester->run(['command' => 'test:hello']);
        
        $this->assertEquals(0, $this->tester->getStatusCode());
        $this->assertStringContains('Hello World!', $this->tester->getDisplay());
    }
    
    public function testHelloCommandWithName(): void
    {
        $this->tester->run([
            'command' => 'test:hello',
            'name' => 'PHP'
        ]);
        
        $this->assertEquals(0, $this->tester->getStatusCode());
        $this->assertStringContains('Hello PHP!', $this->tester->getDisplay());
    }
}

composer.json Configuration

{
    "name": "my/cli-tool",
    "description": "My CLI tool built with Silly",
    "type": "project",
    "require": {
        "php": "^8.0",
        "silly/silly": "^1.8"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "bin": ["cli-app.php"],
    "scripts": {
        "test": "phpunit tests/"
    }
}