こんにちは、かつコーチです。
前回は、このシリーズで扱ってきたテーマの中から、よくあるつまずきポイントをQ&A形式で振り返りました。
ここからは「設計」というテーマに入っていきます。
PHPを学んでいると、必ずと言っていいほど「MVC」という言葉に出会います。
今回は、MVCとは何かという基本の理解から、素のPHPだけでMVCを実現しようとしたときに感じる限界まで、順を追って解説していきます。
MVCとは?
Model・View・Controllerの役割
MVCとは、アプリケーションの処理を「Model」「View」「Controller」という3つの役割に分けて設計する考え方です。
それぞれの役割を簡単にまとめます。
| 要素 | 役割 |
|---|---|
| Model(モデル) | データの取得・保存・加工など、ビジネスロジックを担当する |
| View(ビュー) | 画面に表示するHTMLの見た目を担当する |
| Controller(コントローラ) | リクエストを受け取り、ModelとViewをつなぐ司令塔の役割を担当する |
ユーザーがリクエストを送ると、まずControllerが受け取り、必要なデータをModelに問い合わせ、その結果をViewに渡して画面を作る、という流れになります。
なぜ役割を分ける必要があるのか
「1つのファイルにまとめて書けば楽なのに、なぜわざわざ分けるのか」と疑問に思う方も多いと思います。
理由は、処理を混在させたコードは、後から読むのも直すのも大変になるからです。
私が初めてPHPだけで小さな会員管理システムを作ったとき、1つのPHPファイルの中にデータベースへの接続、SQLの実行、HTMLの出力を全部まとめて書いていました。
最初は数十行だったので問題なかったのですが、機能を追加していくうちにファイルが数百行に膨れ上がり、「どこにHTMLがあって、どこにSQLがあるのか」を目で追うだけで一苦労になりました。
役割ごとにコードを分けておけば、「表示がおかしい→Viewを見る」「データがおかしい→Modelを見る」というように、修正すべき場所をすぐに絞り込めます。
素のPHPでMVCっぽい構造を作ってみる
ディレクトリ構成のイメージ
まずは、素のPHPでMVCの考え方を真似た、簡単なディレクトリ構成を見てみましょう。
project/
├── index.php # 入り口(すべてのリクエストを受け取る)
├── controllers/
│ └── UserController.php
├── models/
│ └── UserModel.php
└── views/
└── user_list.php
Modelの実装例
Modelには、データベースとのやり取りを担当させます。
<?php
// models/UserModel.php
class UserModel
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function getAllUsers(): array
{
$stmt = $this->pdo->query("SELECT id, name FROM users");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
Controllerの実装例
Controllerには、ModelからデータをもらってViewに渡す役割を持たせます。
<?php
// controllers/UserController.php
class UserController
{
private UserModel $userModel;
public function __construct(UserModel $userModel)
{
$this->userModel = $userModel;
}
public function index(): void
{
$users = $this->userModel->getAllUsers();
require __DIR__ . "/../views/user_list.php"; // Viewに$usersを渡す
}
}
Viewの実装例
Viewには、受け取ったデータを表示するHTMLだけを書きます。
<?php
// views/user_list.php
?>
<!DOCTYPE html>
<html lang="ja">
<body>
<h1>ユーザー一覧</h1>
<ul>
<?php foreach ($users as $user): ?>
<li><?php echo htmlspecialchars($user["name"]); ?></li>
<?php endforeach; ?>
</ul>
</body>
</html>
入り口ファイルでつなぎ合わせる
最後に、index.phpでこれらをつなぎ合わせます。
<?php
// index.php
require __DIR__ . "/models/UserModel.php";
require __DIR__ . "/controllers/UserController.php";
$pdo = new PDO("mysql:host=localhost;dbname=sample_db;charset=utf8mb4", "root", "");
$userModel = new UserModel($pdo);
$userController = new UserController($userModel);
$userController->index();
このように、Model・View・Controllerを別ファイルに分けるだけでも、以前よりずっと見通しの良いコードになります。
素のPHPでMVCを組んだときに感じる限界
限界1:ルーティングを自前で管理する大変さ
先ほどの例は「ユーザー一覧を表示する」という1つの機能しかありません。
実際のアプリでは「ユーザー登録」「ユーザー編集」「ユーザー削除」など、URLごとに呼び出すControllerやメソッドを切り替える必要があります。
これを素のPHPでやろうとすると、index.phpの中に条件分岐がどんどん増えていきます。
<?php
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
if ($path === "/users") {
// ユーザー一覧の処理
} elseif ($path === "/users/create") {
// ユーザー登録の処理
} elseif ($path === "/users/edit") {
// ユーザー編集の処理
} elseif ($path === "/users/delete") {
// ユーザー削除の処理
}
// 機能が増えるたびにelseifが増え続ける…
私も実際にこの構造で機能を増やしていったのですが、URLの数が10を超えたあたりから、どこに何を書いたか把握するだけで疲れるようになりました。
限界2:共通処理を毎回書く必要がある
たとえば「ログインしていないユーザーは弾く」「入力値を必ずチェックする」といった共通処理は、本来1箇所にまとめて書きたいものです。
しかし素のPHPでは、こうした共通処理を各Controllerの先頭に毎回コピーして書く、という状態になりがちです。
<?php
// UserController.phpの中
if (!isset($_SESSION["user_id"])) {
header("Location: /login");
exit;
}
// 本来の処理はここから
これと似たようなチェックを、別のControllerにもまた書いて、また別のControllerにも書いて……という状態になると、チェック内容を変更したいときに全ファイルを修正して回る必要が出てきます。
限界3:ファイル間の依存関係が複雑になる
先ほどの例ではrequireでファイルを読み込んでいますが、機能が増えるとファイル同士が複雑に依存し合うようになります。
「このModelはどのControllerから使われているんだっけ?」を追いかけるだけでも一苦労になり、修正の影響範囲が読みにくくなっていきます。
限界を感じることの意味
ここまで見てきたように、素のPHPでもMVCの「考え方」自体は再現できます。
しかし、ルーティング・共通処理の一元化・依存関係の整理といった部分は、自分で仕組みを作り込まない限り解決しません。
この「痒いところに手が届かないもどかしさ」こそが、フレームワークが存在する理由につながっていきます。
まとめ
この記事のポイント
- MVCは、処理をModel・View・Controllerの3つの役割に分けて設計する考え方
- 役割を分けることで、修正すべき場所を素早く絞り込めるようになる
- 素のPHPでもMVCっぽい構造は作れるが、ルーティングや共通処理の管理が煩雑になっていく
- ファイル間の依存関係が複雑になり、機能追加のたびに手間が増えていく
- この限界を体感しておくことが、フレームワークのありがたみを理解する近道になる
次に読むべき記事
次回は、今回感じた限界を実際に手を動かして体験するために、素のPHPで簡易的なルーティングとコントローラの仕組みを自作してみます。
→ 次の記事:素のPHPで簡易フレームワークを自作してみる