この記事は、以下記事に大きく影響を受け、自分の学習用として執筆しました。ぜひ元記事をご覧ください。
目次
- はじめに:あなたのコード、もう破綻してませんか?
- よくある失敗パターン3選
- UseCase層とは?3行で理解する
- 5分でできる実装手順
- 現場で遭遇する3大疑問への回答
- 失敗しないための3つのルール
- チーム導入時の説得テクニック
- まとめ:今日から始められる現実的な一歩
はじめに:あなたのコード、もう破綻してませんか?
「このコントローラ、200行超えてるんだけど...」
「新機能追加するたびにバグが増える...」
「前任者のコード、どこに何があるか分からない...」
このモヤモヤ、実は設計で90%解決できます。
でも「クリーンアーキテクチャ」「DDD」「Repository」...情報が多すぎて何から始めればいいか分からないですよね。特にベンチャーやスタートアップでは**「理想論より今日の納期」**が現実です。
この記事では、Laravelの良さを殺さず、明日から使える超シンプルな設計を紹介します。難しい理論は不要。コピペして5分で試せます。
この記事の対象者
- Laravel初学者〜中級者(実務1年未満)
- 「設計って何?」と思ってる駆け出しエンジニア
- スタートアップで「速度」を求められてる現場の人
よくある失敗パターン3選
❌ 失敗1:ファットコントローラ地獄
class PostController
{
public function store(Request $request)
{
// 権限チェック(20行)
// バリデーション(30行)
// ビジネスロジック(50行)
// DB保存(10行)
// 通知送信(15行)
// レスポンス整形(10行)
// 合計135行!!
}
}
何が起きるか:
- Git競合が頻発
- テストが書けない
- バグ修正で別の機能が壊れる
- 新人が「触りたくない」コードに
❌ 失敗2:ファットモデル沼
class Post extends Model
{
public function store() { }
public function storeWithValidation() { }
public function saveAfterCheck() { }
public function createPost() { }
// どれ使えばいいの...?
// しかも1000行超え...
}
何が起きるか:
- メソッド名の命名で毎回悩む
-
save()と衝突してsavePost()とか苦肉の策 - モデルが何でも屋になる
❌ 失敗3:過度な設計(理想論の罠)
packages/Domain/
packages/UseCase/
packages/Infrastructure/
packages/Presentation/
何が起きるか:
- ファイル数が爆増(10ファイル→50ファイル)
- Eloquentの便利機能が使えない
- チームメンバーがついてこれない
- 「速度」が死ぬ
UseCase層とは?3行で理解する
✅ 1機能 = 1ファイル
✅ ビジネスロジックだけを書く場所
✅ Eloquentをそのまま使える
それだけ。 これがUseCase層です。
ビフォー・アフター
❌ Before(全部コントローラ)
class PostController
{
public function store(Request $request)
{
// 投稿数チェック
$count = Auth::user()->posts()
->where('created_at', '>=', Carbon::today())
->count();
if ($count >= 10) {
throw new Exception('投稿上限');
}
// 保存
$post = new Post($request->all());
$post->user_id = Auth::id();
$post->save();
return $post;
}
}
✅ After(UseCase分離)
// app/UseCases/Post/StoreAction.php
class StoreAction
{
public function __invoke(User $user, array $data): Post
{
// 投稿数チェック
$count = $user->posts()
->where('created_at', '>=', Carbon::today())
->count();
if ($count >= 10) {
throw new PostLimitException('投稿上限');
}
// 保存
$post = new Post($data);
$post->user()->associate($user);
$post->save();
return $post;
}
}
// Controller
class PostController
{
public function store(Request $request, StoreAction $action)
{
$post = $action(Auth::user(), $request->validated());
return new PostResource($post);
}
}
何が変わった?
- ✅ ロジックの場所が明確(
StoreActionにある) - ✅ コントローラは5行!(HTTPの処理だけ)
- ✅ CLIからも再利用可能
- ✅ テストが簡単
5分でできる実装手順
ステップ1:ディレクトリ作成(30秒)
mkdir -p app/UseCases/Post
ステップ2:UseCaseクラス作成(2分)
<?php
namespace App\UseCases\Post;
use App\Models\Post;
use App\Models\User;
class StoreAction
{
/**
* 投稿を作成する
*/
public function __invoke(User $user, array $data): Post
{
$post = new Post($data);
$post->user()->associate($user);
$post->save();
return $post;
}
}
ステップ3:コントローラで使う(1分)
use App\UseCases\Post\StoreAction;
class PostController extends Controller
{
public function store(Request $request, StoreAction $action)
{
$validated = $request->validate([
'title' => 'required|max:100',
'body' => 'required',
]);
$post = $action($request->user(), $validated);
return new PostResource($post);
}
}
ステップ4:動作確認(1分)
php artisan serve
# POSTリクエスト送信 → 動く!
以上!たったこれだけ。
現場で遭遇する3大疑問への回答
Q1. 「結局Serviceと何が違うの?」
A. 粒度と命名規則です。
// ❌ Service(何でも詰め込みがち)
class PostService
{
public function store() { }
public function update() { }
public function delete() { }
public function like() { }
public function share() { }
// 100メソッド...
}
// ✅ UseCase(1アクション1クラス)
StoreAction.php // 投稿作成だけ
UpdateAction.php // 更新だけ
DeleteAction.php // 削除だけ
ルール:1ファイル100行以内を目指す
Q2. 「Repository使わなくていいの?」
A. Laravelなら不要です。
Repositoryパターンは「データベースを抽象化する」ための設計。でもLaravelの現実:
// Repository使うと...
$repository->findByUserAndDate($user, $date);
$repository->findWithCommunity($id);
$repository->findActiveByCategory($category);
// メソッドが無限に増える...
// Eloquent使えば...
Post::where('user_id', $user->id)
->where('created_at', '>=', $date)
->get();
// スコープで自由自在!
Eloquentの強みを殺すな。 これがLaravel流です。
Q3. 「テストどうするの?」
A. 機能テストで十分。
use Illuminate\Foundation\Testing\RefreshDatabase;
class StoreActionTest extends TestCase
{
use RefreshDatabase;
public function test_投稿が作成できる()
{
$user = User::factory()->create();
$action = new StoreAction();
$post = $action($user, [
'title' => 'テスト',
'body' => '本文',
]);
$this->assertDatabaseHas('posts', [
'title' => 'テスト',
'user_id' => $user->id,
]);
}
}
DBアクセス込みでOK。 むしろその方が実用的。
失敗しないための3つのルール
ルール1:1ファイル = 1責務
// ❌ NG:1つのUseCaseで複数のことをする
class PostAction
{
public function storeOrUpdate() { } // ダメ!
}
// ✅ OK:明確に分ける
StoreAction.php
UpdateAction.php
ルール2:例外は同じ階層に置く
app/UseCases/Post/
├── StoreAction.php
├── UpdateAction.php
└── Exceptions/
└── PostLimitException.php ← ここ!
理由: ドメインでまとまるから分かりやすい
ルール3:FormRequestと組み合わせる
// ✅ 役割分担が明確
FormRequest → バリデーション
Policy → 権限チェック
UseCase → ビジネスロジック
Resource → レスポンス整形
Controller → 上記をつなぐだけ
それぞれが100行以内!
チーム導入時の説得テクニック
パターン1:小さく始める
❌「全コントローラをリファクタします!」
✅「新機能だけUseCaseで書いてみます」
段階的導入が鉄則。 いきなり全部変えると反発されます。
パターン2:数字で示す
| 項目 | Before | After |
|---|---|---|
| コントローラの平均行数 | 180行 | 30行 |
| Git競合の発生率 | 週3回 | 週0.5回 |
| 新機能の実装時間 | 2日 | 1日 |
「速くなった」を可視化する。
パターン3:困ったら記事を見せる
「Laravel界隈で有名なmpywさんも推奨してます」
「Zennで6件のバッジ獲得した記事です」
権威を借りる。 新人の意見より効果的。
実践例:よくある3シーン
シーン1:投稿作成(基本)
class StoreAction
{
public function __invoke(User $user, array $data): Post
{
$post = new Post($data);
$post->user()->associate($user);
$post->save();
return $post;
}
}
シーン2:投稿+画像アップロード(複雑)
class StoreWithImageAction
{
public function __construct(
private ImageUploader $uploader // 外部サービスはDI
) {}
public function __invoke(User $user, array $data, UploadedFile $image): Post
{
// 画像アップロード
$path = $this->uploader->upload($image);
// 投稿作成
$post = new Post($data);
$post->image_path = $path;
$post->user()->associate($user);
$post->save();
return $post;
}
}
シーン3:複数モデルの操作(トランザクション)
class PublishPostAction
{
public function __invoke(Post $post): Post
{
return DB::transaction(function () use ($post) {
// ステータス更新
$post->update(['status' => 'published']);
// 通知作成
Notification::create([
'user_id' => $post->user_id,
'message' => '投稿が公開されました',
]);
return $post;
});
}
}
よくあるトラブルと対処法
トラブル1:「UseCaseが太ってきた」
症状: 1ファイルが200行超え
対処法:
// ❌ 1つのUseCaseに詰め込む
class StoreAction
{
public function __invoke()
{
// バリデーション
// 画像処理
// 保存
// 通知
// 全部で250行...
}
}
// ✅ 処理を分割
class StoreAction
{
public function __construct(
private ValidatePostData $validator,
private ProcessImage $processor,
private NotifyUser $notifier
) {}
public function __invoke()
{
$data = $this->validator->validate();
$image = $this->processor->process();
// 各処理が50行以内!
}
}
トラブル2:「CLIとHTTPで処理が違う」
症状: 同じ機能なのにコードが重複
対処法:
// UseCase(共通処理)
class StoreAction { }
// HTTP
class PostController
{
public function store(Request $request, StoreAction $action)
{
return $action($request->user(), $request->validated());
}
}
// CLI
class ImportPostsCommand extends Command
{
public function handle(StoreAction $action)
{
foreach ($this->getData() as $data) {
$action($user, $data); // 同じUseCaseを使う!
}
}
}
トラブル3:「テストが遅い」
症状: テスト実行に5分かかる
対処法:
// ❌ 毎回マイグレーション実行
use RefreshDatabase;
// ✅ SQLiteのメモリDB使用
// phpunit.xml
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
// テストが10倍速に!
導入のロードマップ(3ステップ)
第1週:1機能だけ試す
- 新規機能をUseCaseで実装
- チームにレビューしてもらう
- フィードバック収集
第2週:既存の1コントローラをリファクタ
- 一番太ってるコントローラを選ぶ
- UseCaseに分割
- Before/After比較を共有
第3週:チームルール化
- 命名規則を決める
- テンプレートを作る
- ドキュメント整備
3週間で定着します。
まとめ:今日から始められる現実的な一歩
覚えることは3つだけ
app/UseCases/を作る- 1アクション = 1クラス
- Eloquentをそのまま使う
この設計のメリット(再確認)
| 項目 | 効果 |
|---|---|
| 🚀 開発速度 | Eloquentの強みを活かせる |
| 📖 可読性 | ファイルが小さく場所が明確 |
| 🔧 保守性 | 修正範囲が限定的 |
| 👥 チーム開発 | Git競合が激減 |
| ✅ テスト | 機能単位でテスト可能 |
いきなり完璧を目指さない
❌「全部リファクタしてから本番」
✅「1機能ずつ、新規から適用」
理想論より現実解。 これがベンチャー・スタートアップで生き残る秘訣です。
最初の一歩
# 今すぐ実行!
mkdir -p app/UseCases/Post
touch app/UseCases/Post/StoreAction.php
たったこれだけで、あなたのコードは変わります。