0.はじめに
こんにちは!KIYO Learningでスタディングの開発をしている @gawa32 です!
「MVCでControllerは薄くしましょう」――よく聞く言葉ですが、実際の開発現場ではどうでしょうか?
気がつけばDB処理や複雑な業務ロジック、さらには多くのバリデーションまでもが詰め込まれて、“だんだん太っていくコントローラ”が出来上がっている…そんな経験、ありませんか?
本記事では、そんな「Fat Controller(ファットコントローラ)」問題をPHPを用いた実例とともに掘り下げ、なぜ起きるのか・どう解決すればいいのかを解説します。

1. 序章:設計思想と現実のギャップ
アプリケーション開発を行う中で、誰しも一度は「MVC」という言葉を聞いたことがあると思います。
Model(モデル)、View(ビュー)、Controller(コントローラー)という役割に責任を分け、コードを整理しやすくする設計思想です。
理想的なMVCの世界では、Controllerはシンプルで、「リクエストを受け取り → Modelに処理を委譲 → Viewに結果を渡す」という流れを担います。つまり、Controllerは“薄く”あるべきというのが基本的な考え方です。
しかし、現実の開発現場ではどうでしょうか?
- 「まずは動くものを優先しよう」
- 「1画面だけだし、ここに全部書いちゃえ」
- 「急ぎの対応だから、後でリファクタリングしよう(←されない)」
…そんな日々の積み重ねによって、Controllerはだんだんと肥大化していきます。
入力チェック、データの整形、条件分岐、例外処理、DBアクセスなど、
さまざまな処理が積み重なり、気がつけば「Controllerがほとんどの処理を担っている」ような状態に。
このような状態のControllerは、Fat Controller(太ったコントローラ) と呼ばれ、保守性・再利用性・テストのしやすさなど、様々な面で問題を引き起こします。
2. Fat Controllerとは?
Fat Controller(太ったコントローラ) とは、その名の通り「太りすぎたコントローラー」のことです。
本来、Controllerはリクエストとレスポンスの橋渡し役に徹するべきですが、現場では処理がどんどんControllerに集まりがちです。
class UserController extends AbstractController
{
public function register(Request $request)
{
$user = new User();
$user->setName($request->get('name'));
$user->setEmail($request->get('email'));
if (!filter_var($user->getEmail(), FILTER_VALIDATE_EMAIL)) {
throw new \Exception('Invalid email');
}
if ($this->userRepository->exists($user->getEmail())) {
throw new \Exception('Already exists');
}
$this->userRepository->save($user);
return new JsonResponse(['message' => 'OK']);
}
}
このControllerでは、リクエストの取得からオブジェクトの生成、入力チェック(バリデーション)、重複チェック、永続化、例外処理までをすべて担っています。
一見シンプルにも見えますが、処理が集中しすぎてしまうと、テストがしづらくなったり、保守性が低下しやすくなるといった課題も生まれてきます。
3. リファクタリングしてみる
この問題を解決するためには、「責務の分離」が重要です。
たとえば、業務ロジックは Service層 に切り出し、Controllerは呼び出しだけに専念します。
class UserController extends AbstractController
{
public function register(Request $request)
{
$this->userService->register(
$request->get('name'),
$request->get('email')
);
return new JsonResponse(['message' => 'OK']);
}
}
class UserService
{
public function __construct(private UserRepository $userRepository)
{
}
public function register(string $name, string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \Exception('Invalid email');
}
if ($this->userRepository->exists($email)) {
throw new \Exception('Already exists');
}
$user = new User();
$user->setName($name);
$user->setEmail($email);
$this->userRepository->save($user);
}
}
これにより、Controllerは薄くなり、テスト・再利用・保守が格段にしやすくなります。
4. よくある悩みと対策
✔ どこまで分ければいいのか?
目安:
分けるべき処理 | Controllerに残してもOK |
---|---|
DBチェック、業務ロジック | パラメータ受け取り、バリデーション、レスポンス処理 |
✔ バリデーションの置き場所
バリデーションをどこに書くかは、チームの方針や設計思想によって分かれるポイントです。
以下はあくまで一例ですが、状況に応じて判断するのが良いと思います。
- Controllerでまとめて書くと、入力とバリデーションの対応が見やすくなる
- Serviceに書くことで、Controllerをスリムに保てる
- 再利用性を考えるなら、バリデーションクラスに切り出すのも選択肢
class RegisterUserValidator
{
public function validate(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \Exception('Invalid email');
}
}
}
✔ Serviceが肥大化してきたら?
- 処理内容ごとにServiceを分割(例:UserService → UserRegistrationService)
- 呼び出し口と処理本体を切り分けるのも有効
✔ 設計ルールはチームで決めよう
- Controller何行超えたら分割?
- Service名や設置場所どうする?
- バリデーションの統一方針は?
設計は「チームの共通認識」があると迷いが減ります。
5. 現場でのバランス感覚
現実の現場では、すべてを理想通りに分離するのが難しい場面もあります。
- 🔥 スピード重視:あとでリファクタしやすいようにだけしておく
- 🧩 小規模ツール:無理に分離せず、将来的な再利用性で判断
- 👥 メンバー構成:スキルや理解度に合わせて、段階的に分離
- ✅ 設計方針の共有:チーム内で「どこまで分けるか」ルール化する
大事なのは完璧な設計ではなく、あとで育てやすいコードを書くことです。
6.まとめ
- Fat Controllerは、設計思想と現実のギャップから生まれる
- サービス層導入で責務を分離することで、保守性・テスト性が大きく改善
- 設計は“バランス感覚”が大事。割り切るところは割り切ってOK
- チームで方針を揃えることで、コードの品質とスピードが両立できる
「気づいたらControllerが太ってる…」というのは誰にでも起こり得ます。
だからこそ、少しずつでも責務の整理を意識していくことが大切です。
KIYOラーニング株式会社について
当社のビジョンは『世界一「学びやすく、分かりやすく、続けやすい」学習手段を提供する』ことです。革新的な教育サービスを作り成長させていく事で、オンライン教育分野でナンバーワンの存在となり、世界に展開していくことを目指しています。
プロダクト
- スタディング:「学びやすく・わかりやすく・続けやすい」オンライン資格対策講座
- スタディングキャリア:資格取得者の仕事探しやキャリア形成を支援する転職サービス
- AirCourse:受け放題の動画研修がついたeラーニングシステム(LMS)
KIYOラーニング株式会社では一緒に働く仲間を募集しています