LaravelのArtisanコマンドでコンソール出力はどうすべきか

Artisanコマンドの処理をサービスクラスに委譲したとき、「どこでコンソール出力すべきか」は実装方針によって3つのパターンに分かれます。結論としては、サービスクラスはロジックに集中させ、出力はコマンド側で行うのが最も保守性が高いです。ただし例外的にサービス側で出力してもよいケースもあります。

🎯 結論:基本は「Artisanコマンド側で出力」

サービスクラスは「ビジネスロジック」に専念させ、コンソール出力(I/O)はコマンドクラスが担当するのがクリーンアーキテクチャ的にも Laravel 的にも自然です。

理由は3つあります。

  • サービスは CLI 以外(Web、Queue)からも呼ばれる可能性がある
  • I/O を含めるとテストがしづらくなる
  • 責務が混ざると再利用性が落ちる

🧩 3つの実装パターンと使い分け

1) コマンド側で出力(推奨)

サービスは結果だけ返し、コマンドが info()error() を呼ぶ。

// app/Console/Commands/ImportUsers.php
public function handle(UserImportService $service)
{
    $result = $service->run();

    $this->info("Imported {$result->count} users.");
}

// app/Services/UserImportService.php
public function run(): ImportResult
{
    // ロジックだけ
    return new ImportResult($count);
}

メリット

  • 責務が明確
  • テストしやすい
  • Web/Queue からも使える

2) サービスに「出力用のインターフェース」を渡す(柔軟)

サービスに $output を渡し、必要なら出力できるようにする。

public function handle(UserImportService $service)
{
    $service->run($this->output);
}

public function run(OutputStyle $output)
{
    $output->writeln('Start importing...');
}

メリット

  • サービス側で進捗を出したいときに便利
  • テスト時はモックできる

デメリット

  • サービスが CLI 依存になる(ただしインターフェースなので軽減)

3) サービス側で Log に書き、コマンド側で表示(間接方式)

サービスはログだけ残し、コマンドが必要に応じて表示。

// Service
Log::info('Import started');

// Command
$this->info('Import started');

メリット

  • サービスは I/O を持たない
  • ログとコンソール出力を分離できる

デメリット

  • 出力の同期が面倒

🧠 非常に重要なポイント

Laravel のサービスクラスは CLI 以外からも呼ばれる前提で設計するのが長期的に強いです。

  • Web コントローラ
  • Queue ジョブ
  • スケジューラ
  • テストコード

これらから呼ばれる可能性があるため、サービスにコンソール出力を埋め込むと再利用性が落ちるのが最大のデメリットです。

👍 実務でのおすすめ構成

責務を分けて長期的に保守しやすい構成を重視するなら、パターン①(コマンド側で出力)が一番おすすめです。

  • サービスはロジックだけ
  • コマンドは I/O だけ
  • テストも書きやすい
  • 後から Queue 化・API 化しやすい

ひとこと

サービス側で進捗を出したいケース(大量データ処理など)があるなら、パターン②の「OutputStyle を渡す」方式が最も柔軟です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です