Laravelを使ってるけどMVCだけじゃ足りない人へ:設計の第一歩「サービス層・DI・DTO」とは?

Laravelを使って開発をしていると、ある段階で「Controllerが太ってきた」「テストがしづらい」「再利用できない」といった課題に直面することがあるのではないでしょうか?それは、MVCの枠組みだけでプロジェクトが進行している場合に起こりやすい問題です。

本記事では、Laravelを使いながらアプリケーションをより保守性の高いものにするための第一歩として、サービス層DI(依存性注入)、**DTO(データ転送オブジェクト)**の3つを中心に解説します。


MVCだけではつらくなる理由

Laravelの公式チュートリアルや多くの初学者向け記事では、Controller内にロジックをまとめて記述するスタイルがよく見られます。しかし、以下のような状況になると限界がやってきます:

  • フォームからのリクエストをそのまま使い、バリデーション、保存処理、通知などすべてControllerでやっている
  • Controllerのメソッドが300行近くに膨らんでいる
  • 似たようなロジックを他のControllerでも書いている

このような状況を「Fat Controller」と呼び、保守性・拡張性・テスト性が著しく低下します


サービス層とは何か?

定義と目的

サービス層(Service Layer)とは、アプリケーションのビジネスロジックをControllerから切り離して、再利用可能な形で定義する場所です。

例えば、「ユーザーを登録して、メールを送って、ログを残す」という一連の処理をControllerにベタ書きするのではなく、UserServiceクラスにまとめます。

役割

  • 複数の操作を1つのロジックとしてまとめる
  • ControllerはHTTPの受け口として最低限の処理に専念
  • ビジネスロジックの単体テストが容易になる

実装例

// app/Services/UserService.php
class UserService
{
    public function register(array $data): User
    {
        $user = User::create($data);
        // メール送信やログ処理などもここに
        return $user;
    }
}

// app/Http/Controllers/UserController.php
public function store(Request $request)
{
    $user = $this->userService->register($request->validated());
    return redirect()->route('users.index');
}

このようにすることで、Controllerは「依頼」だけを行い、処理はServiceに任せる構成になります。


DI(依存性注入)とは?

LaravelにおけるDI

Laravelでは、クラスのコンストラクタで依存クラスを指定すると、自動的にインスタンスを注入(インジェクション)してくれます。これがサービスコンテナ(Service Container)と**IoC(Inversion of Control)**の恩恵です。

なぜDIが必要か?

  • テスト時にMockやStubを渡せるようになる
  • サービスを切り替えることが容易になる(例えば、開発用と本番用)
  • 依存の管理が明示的になり、コードの読みやすさが向上

実例

// UserController.php
class UserController extends Controller
{
    public function __construct(
        protected UserService $userService
    ) {}

    public function store(StoreUserRequest $request)
    {
        $user = $this->userService->register($request->validated());
        return redirect()->route('users.index');
    }
}

DIにより、UserServiceのインスタンス生成を自分でやる必要がなくなり、テスタブルかつ疎結合なコードになります。


DTO(データ転送オブジェクト)とは?

配列渡しの限界

Laravelでは、Requestクラスから配列でデータを受け取り、それをそのままServiceに渡すことが多いですが、これだと以下のような問題があります:

  • 渡す側と受け取る側で項目が合っているか曖昧
  • リファクタ時に不整合が発生しやすい
  • 自己完結性がなく、可読性が低い

DTOの役割

DTO(Data Transfer Object)とは、ある処理に必要なデータだけを集めて運ぶ専用のクラスです。

// RegisterUserDto.php
class RegisterUserDto {
    public function __construct(
        public string $name,
        public string $email,
        public string $password,
    ) {}

    public static function fromRequest(Request $request): self
    {
        return new self(
            $request->input('name'),
            $request->input('email'),
            $request->input('password')
        );
    }
}

DTOを使ったService層の例

public function register(RegisterUserDto $dto): User
{
    return User::create([
        'name' => $dto->name,
        'email' => $dto->email,
        'password' => Hash::make($dto->password)
    ]);
}

MVC + Service + DTO の構成例

app/
├── Http/
│   └── Controllers/UserController.php
├── Services/
│   └── UserService.php
├── DTOs/
│   └── RegisterUserDto.php

このように役割を分けることで、Controllerが驚くほどスリムになります。また、DTOが明示的になることで、IDEの補完や型チェックも働きやすくなります。


設計の改善がもたらす未来

サービス層、DI、DTOを取り入れるだけでも、以下のような恩恵があります:

  • 保守性の向上:修正が局所的で済む
  • 再利用性:同じロジックを別の場所で簡単に呼び出せる
  • テストしやすさ:Service単体でテスト可能
  • 読みやすさ:コードの責務が明確になる

これらはすべて、「1人プロジェクト」でも「チーム開発」でも効果を発揮します。


次に進むなら:UseCase層・ドメイン層・クリーンアーキテクチャへ

今回紹介した設計は、いわば設計の第一歩です。これをきっかけに、次のような概念へとステップアップしていくとよいでしょう:

  • UseCase層の導入:Service層と業務ロジックを分けることで責務を明確に
  • ドメインモデルの導入:ビジネスルールを表現するクラス構造
  • クリーンアーキテクチャ:依存の方向性を意識し、永続層やUI層からロジックを切り離す

Laravelでも十分にクリーンアーキテクチャは実現できます。


まとめ

Laravelは非常に柔軟なフレームワークですが、その柔軟さゆえに「すべてControllerに書いても動く」ことが混乱を招く原因になります。

今回紹介したサービス層・DI・DTOは、保守性・可読性・テスタビリティの面で非常に効果的です。

プロジェクトにアサインされると、どのようなフレームワークを使っているかよりもどのような設計になっているのかを見極めることがとても重要になります。わたしもそこが最初理解できていなかったため、慣れるのに非常に時間がかかりました。でもこれがわかっていると、言語が変わったとしても新しいプロジェクトに入った時に比較的早く馴染むことができます。

あなたがもしLaravelアプリを作っていて何かがが足りたいと思うようであれば、より成長に耐えられる構造になるよう、まずはControllerからビジネスロジックを外に出すことから始めてみるのはいかがですか。

\ 最新情報をチェック /