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 を渡す」方式が最も柔軟です。

