Laravel Zero
コンソールアプリケーション開発のためのマイクロフレームワーク。Laravelの優雅さをCLIアプリケーションにもたらします。
フレームワーク
Laravel Zero
概要
Laravel Zeroは、コンソールアプリケーション開発のためのマイクロフレームワークです。Laravelの優雅さをCLIアプリケーションにもたらし、Laravel開発者に人気が高く、Laravelの機能を活用したスタンドアロンCLIツールの開発に使用されています。豊富なLaravelエコシステムを活用しながら、軽量で高速なCLIアプリケーションを構築できます。
詳細
Laravel Zeroは、Laravelフレームワークのコア機能を活用しながら、CLIアプリケーションに特化して最適化されたフレームワークです。Eloquent ORM、Collection、Queue、Schedule、テストなど、Laravelの強力な機能をコマンドラインアプリケーションで利用できます。
主な特徴
- Laravelエコシステム: Eloquent、Collection、Queueなどの機能を活用
- Artisanコマンド: Laravelと同様のArtisanコマンドラインツール
- 設定管理: Laravelスタイルの設定ファイル管理
- サービスプロバイダー: 依存性注入とサービスコンテナ
- スケジューリング: cron風のタスクスケジューリング
- テスト機能: PHPUnitベースの包括的なテスト機能
- ログ機能: Monologベースの柔軟なログ機能
- 自動更新: セルフアップデート機能
メリット・デメリット
メリット
- Laravel開発者親和性: Laravel開発者には非常に馴染みやすい
- 豊富な機能: Laravelエコシステムの恩恵を受けられる
- 高品質なコード: Laravelの品質基準に基づく設計
- 充実したドキュメント: 詳細で分かりやすい公式ドキュメント
- コミュニティ: Laravel コミュニティからのサポート
- テスト容易性: 組み込まれたテスト機能
- パッケージ配布: Pharファイルとしての配布が可能
デメリット
- 学習コスト: Laravel未経験者には習得に時間が必要
- 依存関係: 多くのLaravelコンポーネントに依存
- ファイルサイズ: 軽量なCLIツールには重い場合がある
- PHP要件: PHP 8.0以上が必要
主要リンク
書き方の例
プロジェクトの作成
composer create-project --prefer-dist laravel-zero/laravel-zero my-cli-app
cd my-cli-app
php application --version
基本的なコマンドの作成
<?php
// app/Commands/HelloCommand.php
namespace App\Commands;
use Illuminate\Console\Scheduling\Schedule;
use LaravelZero\Framework\Commands\Command;
class HelloCommand extends Command
{
/**
* コマンドの名前と引数の定義
*/
protected $signature = 'hello {name : お名前} {--times=1 : 繰り返し回数}';
/**
* コマンドの説明
*/
protected $description = '挨拶を表示します';
/**
* コマンドの実行
*/
public function handle(): int
{
$name = $this->argument('name');
$times = $this->option('times');
for ($i = 0; $i < $times; $i++) {
$this->info("こんにちは、{$name}さん!");
}
return self::SUCCESS;
}
/**
* スケジュールの定義
*/
public function schedule(Schedule $schedule): void
{
// $schedule->command(static::class)->daily();
}
}
複雑なコマンドの例
<?php
// app/Commands/DataProcessCommand.php
namespace App\Commands;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use LaravelZero\Framework\Commands\Command;
class DataProcessCommand extends Command
{
protected $signature = 'data:process
{--source=api : データソース (api|file)}
{--format=json : 出力形式 (json|csv|table)}
{--output= : 出力ファイルパス}
{--limit=10 : 処理件数制限}';
protected $description = 'データを取得・処理・出力します';
public function handle(): int
{
$source = $this->option('source');
$format = $this->option('format');
$output = $this->option('output');
$limit = (int) $this->option('limit');
$this->info('データ処理を開始します...');
// データ取得
$data = $this->fetchData($source, $limit);
if ($data->isEmpty()) {
$this->error('データが取得できませんでした');
return self::FAILURE;
}
$this->info("取得件数: {$data->count()}件");
// データ処理
$processedData = $this->processData($data);
// 出力
$this->outputData($processedData, $format, $output);
$this->info('データ処理が完了しました');
return self::SUCCESS;
}
protected function fetchData(string $source, int $limit): Collection
{
$this->task('データ取得中', function () use ($source, $limit) {
sleep(1); // 処理をシミュレート
});
switch ($source) {
case 'api':
return $this->fetchFromApi($limit);
case 'file':
return $this->fetchFromFile($limit);
default:
return collect();
}
}
protected function fetchFromApi(int $limit): Collection
{
try {
$response = Http::get('https://jsonplaceholder.typicode.com/posts', [
'_limit' => $limit
]);
return collect($response->json());
} catch (\Exception $e) {
$this->error("API取得エラー: {$e->getMessage()}");
return collect();
}
}
protected function fetchFromFile(int $limit): Collection
{
// サンプルデータを生成
return collect(range(1, $limit))->map(function ($i) {
return [
'id' => $i,
'title' => "サンプルタイトル {$i}",
'body' => "サンプル本文 {$i}",
'userId' => rand(1, 10)
];
});
}
protected function processData(Collection $data): Collection
{
return $data->map(function ($item) {
return [
'id' => $item['id'],
'title' => str($item['title'])->title(),
'body' => str($item['body'])->limit(50),
'user_id' => $item['userId'],
'processed_at' => now()->toDateTimeString()
];
});
}
protected function outputData(Collection $data, string $format, ?string $output): void
{
switch ($format) {
case 'json':
$content = $data->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
case 'csv':
$content = $this->toCsv($data);
break;
case 'table':
$this->table(
['ID', 'タイトル', '本文', 'ユーザーID', '処理日時'],
$data->map(fn($item) => array_values($item))->toArray()
);
return;
default:
$this->error("未対応の形式: {$format}");
return;
}
if ($output) {
file_put_contents($output, $content);
$this->info("ファイルに出力しました: {$output}");
} else {
$this->line($content);
}
}
protected function toCsv(Collection $data): string
{
if ($data->isEmpty()) {
return '';
}
$headers = array_keys($data->first());
$csv = implode(',', $headers) . "\n";
foreach ($data as $row) {
$csv .= implode(',', array_map(fn($value) => '"' . str_replace('"', '""', $value) . '"', array_values($row))) . "\n";
}
return $csv;
}
}
インタラクティブなコマンドの例
<?php
// app/Commands/SetupCommand.php
namespace App\Commands;
use LaravelZero\Framework\Commands\Command;
class SetupCommand extends Command
{
protected $signature = 'setup';
protected $description = 'アプリケーションの初期設定を行います';
public function handle(): int
{
$this->info('🚀 アプリケーション初期設定');
$this->newLine();
// 基本情報の収集
$appName = $this->ask('アプリケーション名を入力してください', 'My CLI App');
$version = $this->ask('バージョンを入力してください', '1.0.0');
// 選択式質問
$environment = $this->choice(
'実行環境を選択してください',
['development', 'production', 'testing'],
'development'
);
// データベース設定
$useDatabase = $this->confirm('データベースを使用しますか?', false);
$dbConfig = [];
if ($useDatabase) {
$dbConfig = [
'driver' => $this->choice('データベースタイプ', ['mysql', 'postgresql', 'sqlite'], 'sqlite'),
'host' => $this->ask('ホスト', 'localhost'),
'database' => $this->ask('データベース名', 'database'),
'username' => $this->ask('ユーザー名', 'root'),
'password' => $this->secret('パスワード'),
];
}
// ログ設定
$logLevel = $this->choice(
'ログレベルを選択してください',
['debug', 'info', 'warning', 'error'],
'info'
);
// 確認
$this->newLine();
$this->info('設定内容を確認してください:');
$this->table(
['設定項目', '値'],
[
['アプリケーション名', $appName],
['バージョン', $version],
['環境', $environment],
['データベース', $useDatabase ? 'あり' : 'なし'],
['ログレベル', $logLevel],
]
);
if (!$this->confirm('この設定で続行しますか?', true)) {
$this->warn('設定をキャンセルしました');
return self::FAILURE;
}
// 設定ファイルの生成
$this->task('設定ファイル生成中', function () use ($appName, $version, $environment, $dbConfig, $logLevel) {
$config = [
'app' => [
'name' => $appName,
'version' => $version,
'env' => $environment,
],
'database' => $dbConfig,
'logging' => [
'level' => $logLevel,
],
];
file_put_contents(
base_path('.env.generated'),
$this->generateEnvFile($config)
);
sleep(1); // 処理をシミュレート
});
$this->info('✅ 初期設定が完了しました!');
$this->info('生成されたファイル: .env.generated');
$this->warn('手動で .env ファイルに設定を反映してください。');
return self::SUCCESS;
}
protected function generateEnvFile(array $config): string
{
$env = "# Generated configuration\n";
$env .= "APP_NAME=\"{$config['app']['name']}\"\n";
$env .= "APP_VERSION={$config['app']['version']}\n";
$env .= "APP_ENV={$config['app']['env']}\n";
$env .= "\n";
if (!empty($config['database'])) {
$env .= "DB_CONNECTION={$config['database']['driver']}\n";
$env .= "DB_HOST={$config['database']['host']}\n";
$env .= "DB_DATABASE={$config['database']['database']}\n";
$env .= "DB_USERNAME={$config['database']['username']}\n";
$env .= "DB_PASSWORD={$config['database']['password']}\n";
$env .= "\n";
}
$env .= "LOG_LEVEL={$config['logging']['level']}\n";
return $env;
}
}
バックグラウンドタスクとスケジューリング
<?php
// app/Commands/ScheduleCommand.php
namespace App\Commands;
use Illuminate\Console\Scheduling\Schedule;
use LaravelZero\Framework\Commands\Command;
class MaintenanceCommand extends Command
{
protected $signature = 'maintenance:run {--dry-run : 実際には実行せずに表示のみ}';
protected $description = 'メンテナンスタスクを実行します';
public function handle(): int
{
$dryRun = $this->option('dry-run');
if ($dryRun) {
$this->warn('ドライランモード: 実際の処理は実行されません');
}
$this->info('メンテナンスタスクを開始します...');
// ログファイルのクリーンアップ
$this->cleanupLogs($dryRun);
// 一時ファイルの削除
$this->cleanupTempFiles($dryRun);
// データベースの最適化
$this->optimizeDatabase($dryRun);
$this->info('メンテナンスタスクが完了しました');
return self::SUCCESS;
}
public function schedule(Schedule $schedule): void
{
// 毎日午夜2時に実行
$schedule->command(static::class)->dailyAt('02:00');
// 毎週日曜日にドライランモードで実行
$schedule->command(static::class, ['--dry-run'])->weekly();
}
protected function cleanupLogs(bool $dryRun): void
{
$this->task('ログファイルのクリーンアップ', function () use ($dryRun) {
$logPath = storage_path('logs');
$oldLogs = glob("{$logPath}/*.log");
$cleaned = 0;
foreach ($oldLogs as $log) {
if (filemtime($log) < strtotime('-30 days')) {
if (!$dryRun) {
unlink($log);
}
$cleaned++;
}
}
if ($dryRun) {
$this->comment("削除対象ログファイル: {$cleaned}件");
} else {
$this->comment("削除したログファイル: {$cleaned}件");
}
});
}
protected function cleanupTempFiles(bool $dryRun): void
{
$this->task('一時ファイルの削除', function () use ($dryRun) {
$tempPath = sys_get_temp_dir();
$pattern = "{$tempPath}/app_temp_*";
$tempFiles = glob($pattern);
$cleaned = count($tempFiles);
if (!$dryRun) {
foreach ($tempFiles as $file) {
if (is_file($file)) {
unlink($file);
}
}
}
$this->comment($dryRun ? "削除対象: {$cleaned}件" : "削除完了: {$cleaned}件");
});
}
protected function optimizeDatabase(bool $dryRun): void
{
$this->task('データベース最適化', function () use ($dryRun) {
if ($dryRun) {
$this->comment('データベース最適化をスキップ(ドライラン)');
return;
}
// 実際のデータベース最適化処理
// DB::statement('OPTIMIZE TABLE users');
$this->comment('データベース最適化完了');
});
}
}
テストの例
<?php
// tests/Feature/HelloCommandTest.php
namespace Tests\Feature;
use Tests\TestCase;
class HelloCommandTest extends TestCase
{
public function test_hello_command_with_name(): void
{
$this->artisan('hello', ['name' => 'Laravel'])
->expectsOutput('こんにちは、Laravelさん!')
->assertExitCode(0);
}
public function test_hello_command_with_times_option(): void
{
$this->artisan('hello', ['name' => 'Test', '--times' => 3])
->expectsOutput('こんにちは、Testさん!')
->expectsOutput('こんにちは、Testさん!')
->expectsOutput('こんにちは、Testさん!')
->assertExitCode(0);
}
}
アプリケーション設定
<?php
// config/app.php
return [
'name' => env('APP_NAME', 'Laravel Zero'),
'version' => app('git.version'),
'env' => env('APP_ENV', 'production'),
'providers' => [
App\Providers\AppServiceProvider::class,
],
];
カスタムサービスプロバイダー
<?php
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// サービスの登録
$this->app->singleton('custom.service', function ($app) {
return new \App\Services\CustomService();
});
}
public function boot(): void
{
// 起動時の処理
}
}
ビルドとコンパイル
# Pharファイルの作成
php application app:build my-app
# 実行可能ファイルの作成
php application app:build my-app --build-version=1.0.0
# リリース用ビルド
php application app:build my-app --optimize