0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravel設計の現実解:新人でも5分で実践できるUseCase層の導入

Posted at

この記事は、以下記事に大きく影響を受け、自分の学習用として執筆しました。ぜひ元記事をご覧ください。


目次

  1. はじめに:あなたのコード、もう破綻してませんか?
  2. よくある失敗パターン3選
  3. UseCase層とは?3行で理解する
  4. 5分でできる実装手順
  5. 現場で遭遇する3大疑問への回答
  6. 失敗しないための3つのルール
  7. チーム導入時の説得テクニック
  8. まとめ:今日から始められる現実的な一歩

はじめに:あなたのコード、もう破綻してませんか?

「このコントローラ、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つだけ

  1. app/UseCases/を作る
  2. 1アクション = 1クラス
  3. Eloquentをそのまま使う

この設計のメリット(再確認)

項目 効果
🚀 開発速度 Eloquentの強みを活かせる
📖 可読性 ファイルが小さく場所が明確
🔧 保守性 修正範囲が限定的
👥 チーム開発 Git競合が激減
✅ テスト 機能単位でテスト可能

いきなり完璧を目指さない

❌「全部リファクタしてから本番」
✅「1機能ずつ、新規から適用」

理想論より現実解。 これがベンチャー・スタートアップで生き残る秘訣です。

最初の一歩

# 今すぐ実行!
mkdir -p app/UseCases/Post
touch app/UseCases/Post/StoreAction.php

たったこれだけで、あなたのコードは変わります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?