本投稿は TECOTEC Advent Calendar 2025 の21日目の記事です。
こんにちは、証券フロンティア事業部の山本です。
普段の業務は、PHPでAPIやバッチの開発を行っています。
皆さんはMVCフレームワークで開発をしていて、「コントローラー(Controller)がどんどん長くなって読みづらい…」 と感じたことはありませんか?
プログラミングを学び始めた頃や、チュートリアル通りに書いている時は、「動くこと」が最優先です。しかし、実務で開発が進むと、「機能を追加したいのに、どこを直せばいいかわからない」「一箇所直すと他が壊れる」といった悩みにぶつかることがあります。
今回は、Laravelを使用し、よくある「ユーザー新規登録」を題材に、「処理を適切な場所に分ける(レイヤー分け)」 という考え方を紹介します。
よくある「全部入り」のコード
まずは、Laravelの入門記事などでよく見かける書き方を見てみましょう。
今回は、 「メール認証フロー」(登録ボタンを押すと、認証用リンクがついたメールが送信され、それをクリックすると登録完了となる仕組み)を想定します。
フロー (Before)
コントローラーが全ての処理を一人で抱え込んでいます。

現状のコード
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つのクラスを作ります。
- UseCase(ユースケース): 「登録処理の手順」を書く場所。ビジネスロジック。
- Repository(リポジトリ): 「データベースの操作」だけをする場所。
- Service(サービス): データベース以外の具体的な処理(メール送信や外部API連携など)を行う場所。
フロー(After)
コントローラーは「受付」に徹し、実際の作業は専門のクラスに任せます。

リファクタリング
方針が決まったので、クラスを作成していきます。
今回はこの形で配置していきます。
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