はじめに
ビジネスロジックをどこに配置するかは永遠のテーマだと思います。
今回のプロジェクトでは、1クラスで1個の事しかしないUseCaseクラスを採用することにしました。
「1個の事」を何のまとまりとするかは難しいのですが、そこに関しては各々に任せています。
業務上の1つのまとまった操作を原則で考えてます。
ルール
- 配置場所: app/UseCases/<ドメイン名>/
- クラス名: 動詞で始まり「Action」で終わる
- メソッド: publicメソッドはhandle()のみ
- privateメソッドは複数実装可
- 引数が複雑な場合はDTOも活用する
活用例
$action = new CreatePostAction($huga,$hoge);
$action->handle();
作ったもの
インターフェース
UseCaseの呼び出し方を統一したかったので、インターフェースを作成。
app/UseCases/UseCaseHandler.php
namespace App\UseCases;
interface UseCaseHandler
{
public function handle();
}
UseCase作成コマンド
人間の手で作成した頃はこのコマンドを叩いて使っていましたが、AI (自分の場合Claude Code)を活用するようになってからはClaude Codeのカスタムコマンドに組み込んでいます。
PostCreateAction を作成
$ php artisan make:usecase Post/PostCreate
app/Console/Commands/MakeUseCaseCommand.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
class MakeUseCaseCommand extends Command
{
// コマンドのシグネチャと説明
protected $signature = 'make:usecase {name}';
protected $description = 'Create a new use case class with a handle method';
protected $files;
public function __construct(Filesystem $files)
{
parent::__construct();
$this->files = $files;
}
public function handle()
{
// UseCaseInterface の存在チェックと生成
$this->createUseCaseInterfaceIfNotExists();
// 引数からディレクトリとクラス名を取得(例: Customer/DeleteCustomer)
$name = $this->argument('name');
$parts = explode('/', $name);
$classBase = array_pop($parts);
$className = $classBase.'Action';
// app/UseCases 以下のパスを生成
$directoryPath = app_path('UseCases/'.implode('/', $parts));
$fullPath = $directoryPath.'/'.$className.'.php';
// 同一ファイルが存在する場合はエラー
if ($this->files->exists($fullPath)) {
$this->error("UseCase already exists at {$fullPath}!");
return;
}
// ディレクトリが存在しなければ作成
if (! $this->files->isDirectory($directoryPath)) {
$this->files->makeDirectory($directoryPath, 0755, true);
}
// 名前空間の組み立て(例: App\UseCases\Customer)
$namespace = 'App\\UseCases';
if (! empty($parts)) {
$namespace .= '\\'.implode('\\', $parts);
}
// スタブ(テンプレート)の取得と置換
$stub = $this->getStub();
$stub = str_replace('{{ namespace }}', $namespace, $stub);
$stub = str_replace('{{ class }}', $className, $stub);
// ファイルの生成
$this->files->put($fullPath, $stub);
$this->info("UseCase created successfully at {$fullPath}");
}
/**
* UseCaseInterface が存在しなければ生成する
*/
protected function createUseCaseInterfaceIfNotExists()
{
$interfacePath = app_path('UseCases/UseCaseHandler.php');
if (! $this->files->exists($interfacePath)) {
// ディレクトリがなければ作成
$interfaceDir = dirname($interfacePath);
if (! $this->files->isDirectory($interfaceDir)) {
$this->files->makeDirectory($interfaceDir, 0755, true);
}
$interfaceStub = <<<'STUB'
<?php
namespace App\UseCases;
interface UseCaseHandler
{
public function handle();
}
STUB;
$this->files->put($interfacePath, $interfaceStub);
$this->info("UseCaseInterface created successfully at {$interfacePath}");
}
}
/**
* UseCase のスタブテンプレート
*
* @return string
*/
protected function getStub()
{
return <<<'STUB'
<?php
namespace {{ namespace }};
use App\UseCases\UseCaseHandler;
/**
* TODO: どういうユースケースなのか記載
*/
final class {{ class }} implements UseCaseHandler
{
public function __construct()
{
}
public function handle()
{
// TODO: ロジックを書くます
}
}
STUB;
}
}
1年程運用し感じたメリット
人間から見て
- ドメインごとに整理され、クラス名が動詞で始まるため、目的の処理を見つけやすい
- 1クラス1責務なので、無関係なメソッドが視界に入らず、コードレビューがしやすい
AI活用開発
- UseCase内に処理が閉じているため、AIが余計な箇所を変更してしまうリスクがない
- 数千行のServiceクラスと比べて文脈を理解しやすく、意図通りの修正が得られやすい
- ファイルサイズが小さいため、コンテキストを圧迫せず、複数ファイルを同時に扱える
- 影響範囲が明確で、意図しない変更を防げる(大きなクラスでコードを勝手に削除された経験あり)
おまけ