「LaravelのControllerが太ってきた…」
「リファクタリング記事は多いけど、ServiceだのRepositoryだのActionだの…結局どれからやればいいの?」
「技術力に自信ないし、いきなり変なことしてチームに迷惑かけたくない…」
そんな悩みを持つ、特に現場に入ったばかりの初心者・若手エンジニアさんに向けて、以下2点に絞って徹底的にわかりやすく解説します。
- Controller肥大化解消の「最初の一歩」
- チームを巻き込む「現実的な進め方」
この記事を読めば、小難しいアーキテクチャ論に惑わされず、5分後にはあなたのチームのコードをちょっとだけ良くする具体的な行動を起こせるようになります。
📖 目次
-
はじめに:なぜこの記事を書いたか
- 情報が多すぎて迷子になる問題
- 大事なのは「技術」より「チーム貢献」
-
そもそも論:なぜControllerは太るのか? なぜダメなのか?
- Controllerって何?(レストランのウェイター)
- なぜ太る?(敏腕すぎるウェイター問題)
- なぜ太ると困る?(チームが回らなくなる)
-
対策は色々あるけれど…(全体像を俯瞰)
- よく見る「すごい道具」たち
- 初心者が混乱する理由
-
処方箋①:最初の一歩は「Form Request」で決まり!
- なぜ「Form Request」が最強の第一歩なのか?
- 【超具体例】5分で実践!バリデーションを分離しよう
- メリットと注意点
-
処方箋②:次の一歩は「Service層(的なもの)」
- 「的なもの」でOK!ハードルを下げよう
- 【超具体例】ロジックを「お引越し」させよう
- メリットと二次被害(Service層の肥大化)
-
最重要:技術力より大切な「チームでの進め方」
- あるあるな落とし穴:「いきなりリファクタリング」はNG!
- 現実的な「3ステップ」提案
- 技術力がなくてもできる「最高の組織貢献」とは?
- まとめ:今日からあなたができること
🚀 はじめに:なぜこの記事を書いたか
情報が多すぎて迷子になる問題
LaravelのController肥大化対策を調べると、本当にたくさんの情報が出てきます。
-
Service層を作ろう -
Repositoryパターンを導入しよう -
Actionクラスで単一責務にしよう -
Traitで共通処理をまとめよう - いやいや、
DDD(ドメイン駆動設計)だ!
どれも正しく、素晴らしい手法です。しかし情報の多さゆえに、以下のように初心者さんを混乱させます。
- 「わかった、でも結局どれからやればいいの?」
- 「全部やらないとダメなの?」
- 「ウチのチーム、そんなルールないけど…」
大事なのは「技術」より「チーム貢献」
この記事で一番伝えたいのは、「完璧なアーキテクチャを導入すること」が目的ではないということです。
私たちエンジニアの目的は、
- コードを読みやすくして、未来の自分やチームメンバーが困らないようにする
- コードを修正しやすくして、バグを減らし、開発スピードを上げる
- チーム全体で気持ちよく開発できるようにする
ことです。つまり、チームや組織への貢献こそが最重要です。
この記事では、技術的に100点満点を目指すのではなく、チームに貢献するための「現実的な最初の一歩」にフォーカスします。技術力に自信がなくても大丈夫。一緒に学んでいきましょう。
🍔 そもそも論:なぜControllerは太るのか? なぜダメなのか?
中学生でもわかるように、「レストラン」に例えてみましょう。
Controllerって何?(レストランのウェイター)
Laravel(Webアプリケーション)をレストランだとすると、Controller(コントローラー)は「ウェイター」さんのようなものです。
- お客さん(ユーザー)から「ハンバーグください(リクエスト)」と注文を受け取り、
- 厨房(ModelやService)に「ハンバーグ1丁!(処理の依頼)」と伝え、
- できた料理(データ)をお客さん(ユーザー)に「お待たせしました(レスポンス)」と運ぶ。
これがControllerの本来の仕事です。
なぜ太る?(敏腕すぎるウェイター問題)
しかし、プロジェクトが忙しくなると、このウェイターさんが頑張りすぎてしまいます。
「お客さんの注文内容(リクエスト)が正しいか、俺が全部チェックするか…(バリデーション)」
「ソースも俺が作っちゃえ!(ビジネスロジック)」
「食材庫(DB)から材料(データ)を取ってきたり、在庫を管理するのも俺がやる!(DB操作)」
「あ、お客さんからクレームだ。俺が対応しよう!(エラー処理)」
これが「Controllerの肥大化」です。
Laravelは柔軟なので、Controllerに何でも書けてしまいます。だから、ついついウェイターさん(Controller)に色々な仕事を詰め込んでしまうのです。
なぜ太ると困る?(チームが回らなくなる)
ウェイターさんが全部やっているレストラン、どうなるでしょう?
-
読みにくい(新人が困る):
新しいバイト(新人エンジニア)が入ってきても、ウェイターさんの仕事(Controllerのコード)が複雑すぎて、何をやっているのか理解できません。「注文(リクエスト)受けてから、料理出す(レスポンス)までに、何行コード読めばいいんだ…」 -
修正しにくい(バグを生む):
「ハンバーグのソースだけ変えて」と言われても、ウェイターさんが色々な作業(ロジック)の合間にソースを作っている(コードが散らばっている)ため、どこを直せばいいか分かりません。下手に直すと、別の料理(機能)に影響が出る(バグる)かもしれません。 -
テストしにくい(品質が下がる):
ウェイターさんの動き(Controllerの処理)が複雑すぎて、「ちゃんと注文通り動いてるか」のテストが非常に困難になります。
つまり、Controllerが太ると、チーム開発の効率が著しく下がり、品質も担保できなくなるのです。これは組織にとって大きな損失です。
📚 対策は色々あるけれど…(全体像を俯瞰)
肥大化対策として、先ほども挙げたような「すごい道具(設計手法)」がたくさんあります。
- Form Request: 注文チェック(バリデーション)を「受付係」に任せる
- Service層: メインディッシュ作り(ビジネスロジック)を「コック長」に任せる
- Repository層: 食材管理(DB操作)を「食材管理係」に任せる
- Actionクラス: 「ハンバーグを作る」という一連の作業を一つの「レシピ(クラス)」にまとめる
初心者が混乱する理由
これらはどれも、ウェイターさん(Controller)の仕事を「分担」するための素晴らしい考え方です。
しかし、初心者が混乱するのは、
- 「どれがウチの店(プロジェクト)に今、必要なの?」
- 「全部導入しないとダメなの?」
- 「導入の仕方が難しそう…」
と感じてしまうからです。
大丈夫です。全部を一気にやる必要はありません。最も簡単で、最も効果があり、最もチームに迷惑をかけない(むしろ喜ばれる)方法から始めましょう。
💊 処方箋①:最初の一歩は「Form Request」で決まり!
もしあなたのControllerが少しでも太ってきたと感じたら、問答無用で「Form Request」の導入から始めてください。
これは、ウェイターさんの仕事のうち、**「注文内容(リクエスト)が正しいかチェックする(バリデーション)」**という作業を、専門の「受付係」に任せるイメージです。
なぜ「Form Request」が最強の第一歩なのか?
-
責務が超明確:
「リクエストの検証」という仕事以外はしません。非常に分かりやすい。 -
Laravelの標準機能:
難しいライブラリ導入や設定は不要。Laravelが最初から用意してくれている機能です。 -
導入が超カンタン:
後述しますが、本当に5分でできます。 -
効果がデカい:
Controllerからごちゃごちゃしたバリデーションルール($request->validate(...)など)が一掃され、劇的にスッキリします。 -
チームの合意が得やすい:
「バリデーションはここに書く」というルールは、誰にとっても分かりやすく、反対する理由がほとんどありません。
【超具体例】5分で実践!バリデーションを分離しよう
例として、簡単な「ブログ記事投稿(storeメソッド)」を考えてみましょう。
【BEFORE】よくある太ったController
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request; // ← 注目
use App\Models\Post;
class PostController extends Controller
{
public function store(Request $request) // ← 注目
{
// 👎 バリデーションがControllerにベタ書き!
$validated = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
'category_id' => 'required|integer|exists:categories,id',
'tags' => 'nullable|array',
]);
// ログインユーザーのIDを取得
$validated['user_id'] = auth()->id();
// データの登録処理
$post = Post::create($validated);
// レスポンス
return redirect()->route('posts.show', $post)->with('success', '記事を投稿しました!');
}
}
これでも動きますが、validate部分が長くなると、どんどん読みにくくなります。
【AFTER】Form Requestを使って分離!
ステップ1:Form Requestクラスを作る
ターミナル(黒い画面)で、以下のコマンドを実行します。
php artisan make:request StorePostRequest
これだけで、app/Http/Requests/StorePostRequest.php というファイルが自動で作られます。
ステップ2:作られたファイルを編集する
app/Http/Requests/StorePostRequest.php を開いて、2箇所を編集します。
// app/Http/Requests/StorePostRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
// ここを `true` に変更!
// (本来は「この記事を投稿する権限があるか?」をチェックする場所)
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
// Controllerに書いてあったルールを、そっくりそのままコピペ!
return [
'title' => 'required|string|max:255',
'body' => 'required|string',
'category_id' => 'required|integer|exists:categories,id',
'tags' => 'nullable|array',
];
}
}
ステップ3:Controllerを書き換える
最後に、PostControllerのstoreメソッドを書き換えます。
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
// ↓ 使うクラスが変わる!
use App\Http\Requests\StorePostRequest; // Request ではなく StorePostRequest
use App\Models\Post;
// Illuminate\Http\Request は不要になるかも
class PostController extends Controller
{
// ↓ メソッドの引数の型を変えるだけ!
public function store(StorePostRequest $request)
{
// 👎 あったはずの $request->validate(...) が・・・
// 👍 消えた!スッキリ!
// バリデーション済みのデータを取得
// ※ `validated()` メソッドで取得できる
$validated = $request->validated();
// ログインユーザーのIDを取得
$validated['user_id'] = auth()->id(); // もしくは $request->user()->id;
// データの登録処理
$post = Post::create($validated);
// レスポンス
return redirect()->route('posts.show', $post)->with('success', '記事を投稿しました!');
}
}
たったこれだけです!
Controllerからバリデーションロジックが消え、storeメソッドは「登録してリダイレクトする」という本来の仕事に集中できるようになりました。
バリデーションが失敗した場合、Laravelが自動でエラーメッセージと共に前のページに戻してくれます。あなたが何もする必要はありません。
メリットと注意点
-
メリット:
- Controllerが劇的にスッキリする(可読性UP!)
- バリデーションルールが1つのファイル(
StorePostRequest)にまとまる(管理しやすい!) - 他のControllerでも同じバリデーションを使いたくなったら、この
StorePostRequestを使い回せる(再利用性UP!)
-
注意点(落とし穴):
-
authorize()メソッドをtrueにし忘れると、「権限がありません(403 Forbidden)」エラーになるので注意。 - バリデーション済みのデータを取得するときは、
$request->all()ではなく$request->validated()を使う癖をつけましょう。(validated()はルールで定義したキーのデータだけを安全に取得してくれます)
-
💊 処方箋②:次の一歩は「Service層(的なもの)」
Form Requestを導入してスッキリしたら、次のステップに進みましょう。
次は、ウェイターさん(Controller)がやっている「メインディッシュ作り(ビジネスロジック)」を、コック長(Service)に任せるイメージです。
「的なもの」でOK!ハードルを下げよう
「Service層(サービスクラス)」と聞くと、難しそうに感じるかもしれません。
厳密な定義(DDDとか)もありますが、ここでは超シンプルに考えましょう。
初心者向けService層の定義:
「Controllerに書くと長くなる、何かしらの複雑な処理を、代わりにやってくれるクラス(ファイル)」
このくらいの理解で十分です。appディレクトリの下にServicesというフォルダを作って、そこにクラスを置けば、もう立派なService層です。
【超具体例】ロジックを「お引越し」させよう
先ほどのPostControllerの例を続けます。
もし「記事を投稿したら、Slackに通知を送り、さらにタグも登録する」みたいな、ちょっと複雑な処理が増えたとします。
【BEFORE】Controllerがまた太ってきた…
// app/Http/Controllers/PostController.php
use App\Http\Requests\StorePostRequest;
use App\Models\Post;
use Illuminate\Support\Facades\Http; // Slack通知用
use App\Models\Tag; // タグ登録用
class PostController extends Controller
{
public function store(StorePostRequest $request)
{
$validated = $request->validated();
$validated['user_id'] = auth()->id();
// 👎 ここから下、Controllerの仕事っぽくない…
// 1. 記事を登録
$post = Post::create($validated);
// 2. タグを登録(複雑な処理)
if (isset($validated['tags'])) {
foreach ($validated['tags'] as $tagName) {
$tag = Tag::firstOrCreate(['name' => $tagName]);
$post->tags()->attach($tag->id);
}
}
// 3. Slackに通知(外部連携)
Http::post('https://hooks.slack.com/services/XXXX', [
'text' => "新しい記事が投稿されました!: {$post->title}",
]);
// レスポンス
return redirect()->route('posts.show', $post)->with('success', '記事を投稿しました!');
}
}
これでは、storeメソッドが「記事の登録」「タグの処理」「Slack通知」という3つ以上のことをやっており、また太ってきました。
【AFTER】Service層(的なもの)にお引越し!
ステップ1:Serviceクラスを作る
app/Services/PostService.php というファイルを手動(またはartisanコマンド)で作ります。
// app/Services/PostService.php
namespace App\Services;
use App\Models\Post;
use App\Models\Tag;
use Illuminate\Support\Facades\Http;
class PostService
{
/**
* 新しい記事を作成し、関連処理を行う
*
* @param array $data (バリデーション済みデータ)
* @param int $userId
* @return Post (作成された記事モデル)
*/
public function createPost(array $data, int $userId): Post
{
$data['user_id'] = $userId;
// 1. 記事を登録
// ※ DBトランザクションなどは、ここに書くとより堅牢になる(今回は省略)
$post = Post::create($data);
// 2. タグを登録
if (isset($data['tags'])) {
$this->attachTags($post, $data['tags']);
}
// 3. Slackに通知
$this->sendSlackNotification($post);
return $post;
}
// privateメソッドにして、処理をさらに分割すると読みやすい
private function attachTags(Post $post, array $tagNames): void
{
foreach ($tagNames as $tagName) {
$tag = Tag::firstOrCreate(['name' => $tagName]);
$post->tags()->attach($tag->id);
}
}
private function sendSlackNotification(Post $post): void
{
Http::post('https://hooks.slack.com/services/XXXX', [
'text' => "新しい記事が投稿されました!: {$post->title}",
]);
}
}
(PostControllerに書いてあったロジックを、ほぼコピペして持ってきただけです)
ステップ2:ControllerからServiceを呼び出す
Controllerを「依存性の注入(DI)」というテクニックを使って書き換えます。難しく聞こえますが、「引数にクラス名を書くだけ」です。
// app/Http/Controllers/PostController.php
use App\Http\Requests\StorePostRequest;
use App\Models\Post;
use App\Services\PostService; // ← Serviceをインポート
class PostController extends Controller
{
// プロパティでServiceを持っておく
private $postService;
// construct(コンストラクタ)でServiceを受け取る
public function __construct(PostService $postService)
{
$this->postService = $postService;
}
public function store(StorePostRequest $request)
{
// 👍「記事を作って!」とコック長(Service)に依頼するだけ!
$post = $this->postService->createPost(
$request->validated(),
$request->user()->id
);
// レスポンス
return redirect()->route('posts.show', $post)->with('success', '記事を投稿しました!');
}
}
どうでしょう? storeメソッドが、
-
StorePostRequestでバリデーション(受付係) -
PostServiceに処理を丸投げ(コック長) - レスポンスを返す(ウェイター)
という、ウェイター本来の仕事に集中できる、超スリムな姿に戻りました!
メリットと二次被害(Service層の肥大化)
-
メリット:
- Controllerが「リクエスト」と「レスポンス」の制御に集中できる。
- ビジネスロジック(
PostService)が独立したので、テストがめちゃくちゃしやすくなる。(←これは品質保証において超重要!) - 他の場所(例えば、バッチ処理やAPI)からも同じ
PostServiceを呼び出せる(再利用性UP!)
-
デメリット・二次被害(落とし穴):
- **「Service層が肥大化する」**という二次被害がよく起こります。
- コック長(
PostService)が、今度は「ユーザー管理(UserService)」や「決済(PaymentService)」までやり始めるようなものです。 -
対策: Service層が太ってきたら、さらに責務を分割(例:
TagServiceやNotificationServiceを作る)ことを検討します。が、それは次のステップ。まずは「Controllerからロジックを分離する」ことが第一目標です。
🌟 最重要:技術力より大切な「チームでの進め方」
さて、ここまで技術的な話をしてきましたが、ここからがこの記事で最も重要なパートです。
どれだけ素晴らしい技術(Form RequestやService層)を知っていても、チームメンバーに相談なしに勝手に導入すると、それは「組織貢献」どころか「迷惑行為」になりかねません。
あるあるな落とし穴:「いきなりリファクタリング」はNG!
技術を学んだばかりの人がやりがちな失敗が、「既存のコードが汚い!」と憤慨し、誰にも相談せず、いきなり大規模なリファクタリング(修正)のプルリクエスト(PR)を投げることです。
-
なぜNGか?
- レビューが地獄: 何十ファイルも変更されたPRは、レビューする側(リーダーや先輩)の時間を大量に奪います。
- バグの温床: 既存の動いているコードを広範囲に変更すると、予期せぬバグ(デグレ)を生む可能性が非常に高くなります。
- 自己満足: チームの合意がない修正は、ただの「自己満足」と捉えられかねません。
私たちは「すごいエンジニア」になるのが目的ではなく、「チームに貢献するエンジニア」を目指しています。
現実的な「3ステップ」提案
では、どうすればチームを巻き込み、円滑に改善を進められるでしょうか?
答えは**「小さく始めて、合意形成をすること」**です。
ステップ1:『気づき』の共有(小さく相談)
まずは「問題提起」です。ただし、絶対にネガティブな言い方をしてはいけません。
-
NG例: 👎
「PostController、ロジックまみれでクソっすね。Form Requestも使ってないし、メンテ不能ですよ。」- (言われた側は「お前が書いたコードだ」とカチンとくるか、「じゃあお前がやれよ」と突き放されます)
-
OK例: 👍
「今、記事投稿の機能改修をしてるんですが、PostControllerのバリデーションが少し長くなってきたなと感じました。もしよければ、次の新しい機能から、Form Requestを使ってバリデーションを分離してみるのはどうでしょう? Controllerがスッキリして見やすくなるかなと思いまして。」- (謙虚に、未来志向で、メリットを添えて提案しています。これなら誰も嫌な気持ちになりません)
ステップ2:『ボーイスカウト・ルール』の適用(小さく実践)
いきなり既存のコードを全部直そうとしてはいけません。
「ボーイスカウト・ルール(来た時よりも美しく)」を実践しましょう。
ボーイスカウト・ルールとは:
「自分が新しく触ったコード(またはその周辺)だけでも、少しだけキレイにしてからコミットする」という考え方。
-
実践例:
リーダーやチームが「あ、それいいね、やってみよう」となったら、まずはあなたが今から実装する「新しい機能」や、今まさに「修正依頼が来ている機能」のControllerから、Form Requestの分離を実践します。 - 既存の、誰も触っていないコードは、今は放置します。それらを直すのは、専用の工数(リファクタリングタスク)が確保されてからです。
ステップ3:『ルール』の明文化(小さく合意)
一度実践して「お、確かにスッキリしていいね!」という空気ができたら、その合意を「チームの資産」に変えましょう。
-
実践例:
- チームのWikiやREADME.mdに、「バリデーションは原則としてForm Requestに記述する」という一文を追記することを提案します。
- PRのテンプレートに「バリデーションはForm Requestに分離されていますか?」というチェック項目を追加するのも良いでしょう。
ここまでくれば、あなたの「小さな改善提案」は、正式な「チームのルール」となり、組織全体のコード品質向上に貢献したことになります。
技術力がなくてもできる「最高の組織貢献」とは?
勘違いしないでほしいのですが、Form RequestやService層を導入すること自体は、高度な技術力ではありません。Laravelの基本機能です。
しかし、
- 「コードが読みにくくなっている」と問題に気づき、
- 「チームのために改善したい」と考え、
- 「Form Requestを使いませんか?」と具体的な解決策を、
- 「謙虚な姿勢で」チームに提案し、合意形成できる
こと。これこそが、技術力が高いだけのエンジニアよりも、ベンチャーやスタートアップの経営層・リーダー層が喉から手が出るほど欲しがる「組織貢献マインド」です。
📑 まとめ:今日からあなたができること
Controllerの肥大化対策は、星の数ほどあります。しかし、初心者がいきなり全部やろうとすると、必ず混乱します。
-
なぜ太る?
- Controller(ウェイター)が頑張りすぎるから。
-
なぜダメ?
- チーム開発の効率(可読性、保守性)が下がるから。
-
最初の一歩は?
-
Form Request(バリデーション分離)で決まり! Laravelの標準機能で、簡単かつ効果絶大。
-
-
次の一歩は?
-
Service層(的なもの)(ビジネスロジック分離)。「Controllerじゃない場所に処理を移す」くらいの軽い気持ちでOK。
-
-
最も大事なことは?
- チームとの合意形成。いきなり直さず、「小さく相談し、小さく実践し、小さくルール化する」こと。
この記事を読んだあなたが5分後にできること。
それは、あなたのプロジェクトのControllerフォルダを開き、validateとベタ書きされているメソッドを1つだけ見つけること。
そして、次のスプリントミーティングや雑談で、リーダーにこう言ってみてください。
「ここのバリデーション、Form Requestに分離してみませんか?」
それが、あなたのチームを、そしてあなた自身を成長させる、最高に価値ある「最初の一歩」です。