Silly
PHPでCLIアプリケーションを作成するためのマイクロフレームワーク。Symfony Consoleをベースに、よりシンプルなAPIを提供。
フレームワーク
Silly
概要
SillyはPHPでCLIアプリケーションを作成するためのマイクロフレームワークです。Symfony Consoleをベースに、よりシンプルなAPIを提供します。シンプルなCLIツールを素早く作成したい開発者に選ばれており、最小限の設定で始められる軽量なフレームワークです。
詳細
SillyはSymfony Consoleコンポーネントのパワーを活用しながら、より直感的で簡潔なAPIを提供します。複雑な設定なしに、関数型のアプローチでコマンドを定義できるため、小さなCLIツールの開発に最適です。
主な特徴
- シンプルなAPI: 最小限のコードでコマンドを定義
- 関数型アプローチ: クロージャーや関数でコマンドロジックを記述
- Symfony Console基盤: 安定したSymfony Consoleの機能を活用
- 軽量: 最小限の依存関係とオーバーヘッド
- 柔軟性: 必要に応じてSymfony Consoleの機能に直接アクセス可能
- PSR準拠: PSR-11コンテナとの統合
- CLI表示: プログレスバー、テーブルなどの豊富な表示機能
メリット・デメリット
メリット
- 学習コストが低い: 非常にシンプルで理解しやすいAPI
- 高速開発: 最小限のコードで素早くCLIツールを構築
- 軽量: 少ない依存関係とメモリ使用量
- Symfony互換: Symfony Consoleの豊富な機能を利用可能
- 柔軟性: 関数型とオブジェクト指向の両方のアプローチをサポート
デメリット
- 機能制限: 大規模で複雑なCLIアプリケーションには不向き
- コミュニティ: 他の主要フレームワークと比べて小さなコミュニティ
- 拡張性: 高度なカスタマイズには限界がある
- ドキュメント: 相対的に少ないドキュメントとリソース
主要リンク
書き方の例
インストールと基本設定
composer require silly/silly
基本的な使用例
#!/usr/bin/env php
<?php
// cli-app.php
require_once 'vendor/autoload.php';
use Silly\Application;
$app = new Application();
// シンプルなコマンド定義
$app->command('hello [name]', function ($name, $output) {
$name = $name ?: 'World';
$output->writeln("Hello $name!");
});
// オプション付きコマンド
$app->command('greet name [--yell]', function ($name, $yell, $output) {
$message = "Hello $name";
if ($yell) {
$message = strtoupper($message);
}
$output->writeln($message);
});
// 複数引数のコマンド
$app->command('sum numbers*', function (array $numbers, $output) {
$sum = array_sum($numbers);
$output->writeln("Sum: $sum");
});
$app->run();
より高度な例
#!/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');
// ファイル処理コマンド
$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>");
// ファイル一覧を取得
$files = glob("$input/*.$format");
if (empty($files)) {
$outputInterface->writeln("<error>No files found with format: $format</error>");
return 1;
}
// プログレスバーを表示
$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); // 処理をシミュレート
}
$progressBar->finish();
$outputInterface->writeln('');
// 結果をテーブルで表示
$table = new Table($outputInterface);
$table->setHeaders(['File', 'Size (bytes)', 'Modified']);
$table->setRows($results);
$table->render();
// ファイル出力
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;
});
// ユーティリティコマンド
$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->command('interactive:setup', function (InputInterface $input, OutputInterface $output) {
$helper = $app->getHelperSet()->get('question');
$output->writeln('<info>Interactive Setup</info>');
$output->writeln('');
// 質問の定義
$questions = [
new \Symfony\Component\Console\Question\Question('Project name: ', 'my-project'),
new \Symfony\Component\Console\Question\Question('Version: ', '1.0.0'),
new \Symfony\Component\Console\Question\ChoiceQuestion(
'Environment: ',
['development', 'production', 'testing'],
'development'
),
new \Symfony\Component\Console\Question\ConfirmationQuestion('Enable debug mode? [y/N] ', false),
];
$answers = [];
foreach ($questions as $question) {
$answers[] = $helper->ask($input, $output, $question);
}
// 設定ファイル生成
$config = [
'name' => $answers[0],
'version' => $answers[1],
'environment' => $answers[2],
'debug' => $answers[3],
];
file_put_contents('config.json', json_encode($config, JSON_PRETTY_PRINT));
$output->writeln('<info>Configuration saved to config.json</info>');
return 0;
});
// バックアップコマンド
$app->command('backup:create source destination [--compress]', function (
$source,
$destination,
$compress,
OutputInterface $output
) {
if (!is_dir($source)) {
$output->writeln("<error>Source directory not found: $source</error>");
return 1;
}
$output->writeln("<info>Creating backup...</info>");
$output->writeln("<comment>Source: $source</comment>");
$output->writeln("<comment>Destination: $destination</comment>");
// バックアップロジック(簡略化)
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS)
);
$totalFiles = iterator_count($files);
$files->rewind();
$progressBar = new ProgressBar($output, $totalFiles);
$progressBar->start();
if (!is_dir($destination)) {
mkdir($destination, 0755, true);
}
foreach ($files as $file) {
$relativePath = str_replace($source . DIRECTORY_SEPARATOR, '', $file->getPathname());
$destPath = $destination . DIRECTORY_SEPARATOR . $relativePath;
$destDir = dirname($destPath);
if (!is_dir($destDir)) {
mkdir($destDir, 0755, true);
}
copy($file->getPathname(), $destPath);
$progressBar->advance();
}
$progressBar->finish();
$output->writeln('');
if ($compress) {
$output->writeln('<info>Compressing backup...</info>');
// 圧縮処理(実装省略)
$output->writeln('<info>Backup compressed</info>');
}
$output->writeln('<info>Backup completed successfully!</info>');
return 0;
});
$app->run();
DIコンテナ統合の例
#!/usr/bin/env php
<?php
// di-example.php
require_once 'vendor/autoload.php';
use Silly\Application;
use Psr\Container\ContainerInterface;
// シンプルなDIコンテナ実装
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]);
}
}
// サービス定義
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 = new SimpleContainer();
$container->set('database', function () {
return new DatabaseService();
});
$container->set('logger', function () {
return new LoggerService();
});
// アプリケーション作成
$app = new Application('DI Example', '1.0.0');
$app->useContainer($container);
// サービスを使用するコマンド
$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->command('db:query sql', function ($sql, DatabaseService $database, LoggerService $logger, $output) {
$logger->log("Executing query: $sql");
$results = $database->query($sql);
foreach ($results as $result) {
$output->writeln($result);
}
$logger->log('Query executed successfully');
});
$app->run();
テストの例
<?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();
// テスト用コマンドを追加
$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の設定例
{
"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/"
}
}