こんにちは、かつコーチです。
前回は、MVCとは何かという基本の考え方と、素のPHPでMVCを組んだときに感じる限界について解説しました。
特に「ルーティングを自前で管理する大変さ」は、実際に手を動かしてみないと実感しにくい部分だと思います。
そこで今回は、素のPHPだけを使って、ごく簡易的な「ルーティング+コントローラ」の仕組みを自作してみます。
いきなりフレームワークの中身をすべて理解するのは大変ですが、簡易版を自分の手で作ってみることで、Laravelのようなフレームワークが裏側で何をしているのかがぐっとイメージしやすくなります。
作るものの全体像
目指すゴール
今回自作するのは、次のようなことができる、最小限のミニフレームワークです。
- URLのパスに応じて、呼び出すControllerとメソッドを自動的に振り分ける(ルーティング)
- 各Controllerは共通の作法でレスポンスを返す
- 存在しないURLにアクセスされたら404ページを表示する
「フレームワーク」と聞くと大掛かりなものを想像するかもしれませんが、本質的な部分だけを取り出せば、実はそれほど巨大な仕組みではありません。
ディレクトリ構成
今回作るミニフレームワークのディレクトリ構成は次の通りです。
mini-framework/
├── index.php # すべてのリクエストの入り口
├── core/
│ └── Router.php # ルーティングの仕組み本体
├── controllers/
│ ├── HomeController.php
│ └── UserController.php
└── views/
├── home.php
└── user_list.php
Step1:すべてのリクエストを1つの入り口に集める
.htaccessでリクエストを集約する
まず、どんなURLへのアクセスも、すべてindex.phpに集める設定をします(Apache環境の場合)。
# .htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
これは「実在するファイルやディレクトリでなければ、すべてindex.phpに転送する」という設定です。
これによって、/usersにアクセスしても/users/editにアクセスしても、実際に処理を開始するのは常にindex.phpになります。
このような「すべてのリクエストの入り口を1箇所に集約する仕組み」は、フロントコントローラパターンと呼ばれています。
Laravelでも、実際のリクエストはすべてpublic/index.phpという1つのファイルを経由しています。
Step2:ルーティングの仕組みを自作する
URLとControllerを対応させるルール表
まずは、「どのURLが来たら、どのControllerのどのメソッドを呼ぶか」というルール表を作ります。
<?php
// core/Router.php
class Router
{
private array $routes = [];
// ルールを登録するメソッド
public function add(string $method, string $path, string $controller, string $action): void
{
$this->routes[] = [
"method" => $method,
"path" => $path,
"controller" => $controller,
"action" => $action,
];
}
// 実際にリクエストを振り分けるメソッド
public function dispatch(string $method, string $path): void
{
foreach ($this->routes as $route) {
if ($route["method"] === $method && $route["path"] === $path) {
$controllerClass = $route["controller"];
$action = $route["action"];
$controller = new $controllerClass();
$controller->$action(); // 該当するControllerのメソッドを呼び出す
return;
}
}
// どのルールにも一致しなかった場合
http_response_code(404);
echo "404 Not Found";
}
}
dispatchメソッドの中では、登録されたルール($this->routes)を1つずつ確認し、アクセスされたパスと一致するものがあれば、対応するControllerのメソッドを呼び出しています。
一致するものがなければ404を返す、というシンプルな仕組みです。
switch文で振り分ける簡易バージョン
先ほどのルール表方式は本格的なフレームワークに近い作りですが、もっとシンプルにswitch文だけで振り分けることもできます。
<?php
// switch文を使った簡易版のルーティング
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
switch ($path) {
case "/":
$controller = new HomeController();
$controller->index();
break;
case "/users":
$controller = new UserController();
$controller->index();
break;
case "/users/create":
$controller = new UserController();
$controller->create();
break;
default:
http_response_code(404);
echo "404 Not Found";
break;
}
まずはこのswitch文版から理解し、慣れてきたらルール表を使ったクラス化バージョンにステップアップする、という流れがおすすめです。
Step3:Controllerを実装する
HomeControllerの実装
トップページ用のControllerを作ります。
<?php
// controllers/HomeController.php
class HomeController
{
public function index(): void
{
$title = "ミニフレームワークへようこそ";
require __DIR__ . "/../views/home.php";
}
}
UserControllerの実装
ユーザー一覧を表示するControllerも作ります。
<?php
// controllers/UserController.php
class UserController
{
public function index(): void
{
// 本来はModelからデータを取得する想定
$users = [
["id" => 1, "name" => "田中"],
["id" => 2, "name" => "佐藤"],
];
require __DIR__ . "/../views/user_list.php";
}
}
Viewファイルの実装
<?php
// views/home.php
?>
<!DOCTYPE html>
<html lang="ja">
<body>
<h1><?php echo htmlspecialchars($title); ?></h1>
</body>
</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>
Step4:入り口ファイルでルールを登録する
最後に、index.phpですべてのファイルを読み込み、ルール表を登録して起動します。
<?php
// index.php
require __DIR__ . "/core/Router.php";
require __DIR__ . "/controllers/HomeController.php";
require __DIR__ . "/controllers/UserController.php";
$router = new Router();
$router->add("GET", "/", "HomeController", "index");
$router->add("GET", "/users", "UserController", "index");
$method = $_SERVER["REQUEST_METHOD"];
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$router->dispatch($method, $path);
これで、/にアクセスすればトップページが、/usersにアクセスすればユーザー一覧が表示される、最小限のミニフレームワークが完成しました。
つまずきやすいポイント:requireの読み込み漏れ
クラスが見つからないエラー
このミニフレームワークを組んでいて、私が実際によくやってしまうミスがあります。
❌ Before:Controllerを追加したのにrequireを忘れる
<?php
// index.php
require __DIR__ . "/core/Router.php";
require __DIR__ . "/controllers/HomeController.php";
// UserController.phpのrequireを書き忘れている
$router = new Router();
$router->add("GET", "/users", "UserController", "index");
// PHP Fatal error: Uncaught Error: Class "UserController" not found
新しいControllerファイルを作ったのに、index.php側でrequireするのを忘れてしまい、Class "UserController" not foundというエラーで数分固まってしまうことがよくありました。
✅ After:新しいファイルを作ったら必ずrequireも追加する
<?php
// index.php
require __DIR__ . "/core/Router.php";
require __DIR__ . "/controllers/HomeController.php";
require __DIR__ . "/controllers/UserController.php"; // 忘れずに追加する
$router = new Router();
$router->add("GET", "/users", "UserController", "index");
このように、ファイルを増やすたびに読み込みの記述も増やさなければならない点は、まさに素のPHPでフレームワークを自作したときに実感する不便さそのものです。
実際のフレームワークでは、このrequireの管理を「オートローディング」という仕組みが自動でやってくれます。
ファイル名とクラス名を一定のルールで揃えておくだけで、requireを1行も書かずに必要なクラスが自動的に読み込まれる、という仕組みです。
今回のミニフレームワークにはオートローディングを実装していませんが、「毎回requireを書くのが地味に面倒くさい」という体感を持ってもらえたら、この項の目的は達成です。
この自作を通して見えてくるもの
フレームワークが解決している課題
今回作ったのは本当に最小限の仕組みですが、それでも次のようなことが見えてきたのではないでしょうか。
- URLとControllerを対応づけるだけでも、地味にコード量が必要になる
- ファイルが増えるたびに、読み込みの管理という新しい手間が発生する
- 404対応や共通処理など、「あって当たり前」の機能も自分で実装しなければならない
普段何気なく使っているフレームワークのルーティング機能は、こうした地味な手間をあらかじめ解決してくれているありがたい存在だということが、実際に手を動かしてみるとよく分かります。
まとめ
この記事のポイント
- フロントコントローラパターンで、すべてのリクエストを1つの入り口に集約する
- ルール表またはswitch文で、URLとControllerのメソッドを対応づける
- Controllerがデータを準備し、Viewに渡して画面を作るという流れはMVCと同じ
- ファイルが増えるたびにrequireの管理という手間が発生することを体感できる
- この不便さの理解が、フレームワークのありがたみを実感する土台になる
次に読むべき記事
自分の手でミニフレームワークを作ってみたことで、フレームワークが解決している課題がより具体的に見えてきたと思います。
次回はいよいよPHP基礎編の最終回として、なぜフレームワークを使うのかを整理していきます。
→ 次の記事:なぜフレームワークを使うのか?Laravelを学ぶ前に知っておきたいこと
