処理の置き場所を整理して、「とりあえず動く」コードから「変更に強い」コードを書く

本投稿は TECOTEC Advent Calendar 2025 の21日目の記事です。

こんにちは、証券フロンティア事業部の山本です。
普段の業務は、PHPでAPIやバッチの開発を行っています。

皆さんはMVCフレームワークで開発をしていて、「コントローラー(Controller)がどんどん長くなって読みづらい…」 と感じたことはありませんか?

プログラミングを学び始めた頃や、チュートリアル通りに書いている時は、「動くこと」が最優先です。しかし、実務で開発が進むと、「機能を追加したいのに、どこを直せばいいかわからない」「一箇所直すと他が壊れる」といった悩みにぶつかることがあります。

今回は、Laravelを使用し、よくある「ユーザー新規登録」を題材に、「処理を適切な場所に分ける(レイヤー分け)」 という考え方を紹介します。

よくある「全部入り」のコード

まずは、Laravelの入門記事などでよく見かける書き方を見てみましょう。
今回は、 「メール認証フロー」(登録ボタンを押すと、認証用リンクがついたメールが送信され、それをクリックすると登録完了となる仕組み)を想定します。

フロー (Before)

コントローラーが全ての処理を一人で抱え込んでいます。

Before_Flow

現状のコード

Model (User.php)
ここでは単なるデータの入れ物として使います。

<?php

class User extends Authenticatable
{
    protected $fillable = ['email', 'password', 'token'];
}

Controller (RegisterController.php)
問題のコントローラーです。
「データの保存」と「メールの構築」と「画面遷移」が混ざり合っています。

<?php

class RegisterController extends Controller
{
    public function store(Request $request)
    {
        // 1. 入力チェック
        $validation = $request->validate([
            'email' => 'required|string|email|unique:users',
            'password' => 'required|string|min:8',
        ]);

        DB::transaction(function () use ($validation) {
            // 2. トークン生成
            $token = Str::random(60);

            // 3. データ保存(仮登録)
            $user = User::create([
                'email' => $validation['email'],
                'password' => Hash::make($validation['password']),
                'token' => $token,
            ]);

            // 4. 認証メール送信(Bladeにトークンを渡してURLを作る)
            Mail::send(
                'emails.register.verify',
                ['token' => $token],
                function ($message) use ($user) {
                    $message->to($user->email)->subject('メールアドレスの確認');
                }
            );
        });

        // 5. 画面遷移
        return view('auth.registered');
    }
}

一見動きますが、もし 「SNSアカウントでも登録できるようにしたい」 となったらどうでしょうか。
「メール送信はいらないけれど、ユーザー保存処理は使い回したい…」と思っても、このコントローラーの中に保存処理が埋め込まれているため、再利用ができません。

役割分担をする

この問題を解決するために、処理を役割ごとに分けます。
今回は以下の3つのクラスを作ります。

  1. UseCase(ユースケース): 「登録処理の手順」を書く場所。ビジネスロジック。
  2. Repository(リポジトリ): 「データベースの操作」だけをする場所。
  3. Service(サービス): データベース以外の具体的な処理(メール送信や外部API連携など)を行う場所。

フロー(After)

コントローラーは「受付」に徹し、実際の作業は専門のクラスに任せます。

After_Flow

リファクタリング

方針が決まったので、クラスを作成していきます。
今回はこの形で配置していきます。

app/
├── Http/
│   └── Controllers/
│       └── RegisterController.php
├── Models/
│   └── User.php
├── Repositories/  <-- 新規作成
│   └── UserRepository.php
├── Services/      <-- 新規作成 
│   └── SystemMailer.php
└── UseCases/      <-- 新規作成
    └── RegisterUserUseCase.php

1. Repository(DB担当)

データの保存や検索など、データベースに関わること(いわゆるCRUD操作)だけを書きます。

<?php

class UserRepository
{
    // ユーザーを作成するだけのメソッド
    public function create(
        string $email,
        string $password,
        ?string $token
    ): User {
        // 重複チェックがあるとなお良し
        return User::create([
            'email' => $email,
            'password' => Hash::make($password),
            'token' => $token,
        ]);
    }
}

2. SystemMailer(メール担当)

メール送信の「機能」だけを提供します。
「誰に」「件名は何で」「どのテンプレートを使って」「何のデータを送るか」は、呼び出し元(UseCase)から受け取ることで汎用性を持たせます。

<?php

class SystemMailer
{
    public function send(
        string $to,
        string $subject,
        string $template,
        array $data = []
    ): void {
        Mail::send(
            $template,
            $data,
            function ($message) use ($to, $subject) {
                $message->to($to)->subject($subject);
            }
        );
    }
}

3. UseCase(ロジック担当)

ここがアプリケーションのメイン処理です。
「トークンを作って、保存して、指定したテンプレートでメールを送る」という手順を記述します。

<?php

class RegisterUserUseCase
{
    // 必要なパーツ(RepositoryとMailer)を受け取る
    public function __construct(
        private UserRepository $userRepository,
        private SystemMailer $mailer
    ) {}

    public function handle(string $email, string $password): void
    {
        DB::transaction(function () use ($email, $password) {
            // 1. トークン生成
            $token = Str::random(60);

            // 2. Repositoryを使って保存
            $user = $this->userRepository->create(
                $email,
                $password,
                $token
            );

            // 3. Mailerを使ってメール送信
            $this->mailer->send(
                to: $email, 
                subject: 'メールアドレスの確認', 
                template: 'emails.register.verify', 
                data: ['token' => $token]
            );
        });
    }
}

4. Controller(受付担当)

最後にコントローラーです。UseCaseを呼び出すだけです。

<?php

class RegisterController extends Controller
{
    // UseCaseを使う準備
    public function __construct(
        private RegisterUserUseCase $useCase
    ) {}

    public function store(Request $request)
    {
        // 入力チェック(ここはFormRequestに切り出すとなお良し)
        $validation = $request->validate([
            'email' => 'required|string|email|unique:users',
            'password' => 'required|string|min:8',
        ]);

        // ユーザー登録
        // コントローラーはUseCaseを呼ぶだけ
        // 「詳しいことはUseCaseにお願いしてあります」という状態
        $this->useCase->handle(
            $validation['email'],
            $validation['password']
        );

        return view('auth.registered');
    }
}

コントローラーから「保存」や「メール」の具体的なコードが消え、何をしているかが一目でわかるようになりました。

「変更に強い」とはどういうことか

さて、ここで開発運用中に以下のような追加要望が出たとします。

「登録者を増やすために、SNSアカウントでも登録できるようにしたい。」

以前の「全部入りコントローラー」だったら、UserRepository がなかったので、保存処理をまたゼロから書く(あるいはコピペする)必要がありました。

しかし、役割分担をした今なら、以下のようにスムーズに対応できます。

SNS登録対応の例

「DBへの保存処理」は既存の UserRepository がそのまま使えます。
新しいUseCaseを作り、そこからRepositoryを呼ぶだけです。

<?php

class RegisterWithSnsUseCase
{
    public function __construct(
        private UserRepository $userRepository // ←既存のクラスを再利用
    ) {}

    public function handle(array $snsData): void
    {
        // SNS特有のデータ整形処理...
        $formattedData = [ 'email' => $snsData['email'], ... ];

        // 既存のリポジトリを使って保存(使い回せる)
        $this->userRepository->create(
            $formattedData['email'],
            Str::random(32),    // SNSにはパスワードがないのでダミーを入れる
            null    // SNS経由ならメール認証済みとして扱うため、トークンは不要
        );
        
        // SNSの場合はメールを送らないので、Mailerは呼ばない
    }
}

このように、コントローラーや既存の登録処理を壊すことなく、新しい機能を安全に追加することができました。

まとめ

  • Controller: 受付係。リクエストを受け取るだけ。
  • UseCase: 手順書。アプリケーションのやりたいことを書く。
  • Repository / Service: 専門職。DBやメール送信機能等の実装を担当。

最初は「ファイルが増えて面倒だな」と思うかもしれません。でも、この形にしておくと、アプリが成長して機能が増えた時に「やっておいてよかった!」と思う瞬間が必ず来ます。

まずは小さな機能から、この「役割分担」を試してみませんか?

テコテックの採用活動について

テコテックでは新卒採用、中途採用共に積極的に募集をしています。
採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。
ご興味を持っていただけましたら是非ご覧ください。 tecotec.co.jp