PHP でルーターを書いてみる
最近 PHP / Laravel を学び始めたのですが、非常に面白そうな動画を見つけたので紹介します。
Use PHP to Create an MVC Framework - Full Course
私の敬愛する Youtube チャンネル freeCodeCamp.org が 2020/10/23 にアップロードした動画です。つい先日に Laravel に触れてみたところブラックボックス感がすごかったので、丁度いい教材になりそうですね。動画で使用している PHP はバージョン 7.4 です。
ちなみに Laravel は Laravel Crash Course 2020 という動画をハンズオンしながら学びました。こちらも PHP のバージョンは 7.3, Laravel は 8.0 と、かなり新しめです。2時間半とかなり短い動画で多くの機能に触れることができるので入門におすすめです。
それでは、フレームワークを作っていきましょう。この記事では
-
環境構築について(動画内で扱っていない)別記事にしました! - ルーティングの基本機能の実装(動画内の序盤で行う)
の2つを簡単に解説します。
プロジェクトの作成
c:\xampp\htdocs
ディレクトリに移動し、プロジェクトを作成します。
cd htdocs
mkdir PHPMVCFramework
cd PHPMVCFramework
VSCode で開発していきましょう。VSCode はすでにインストール済みとします。
code .
これで、後は動画に沿って進めていくだけです。役に立ちそうな拡張機能があればインストールしておきましょう。
Application クラスと Router クラスの作成
ルートフォルダに index.php
を作成して、動作を確認しましょう。
<?php
echo "Hello, World";
php でサーバーを立ち上げるときは以下のコマンドを実行します。
php -S localhost:8080
ちゃんと動作していますね。ただし、ルーターを作成していないので、localhost:8080/hoge
や localhost:8080/fuga
のようなリクエストに対しても、同じ "Hello, world" を返してしまいます。
しかし私達は、
<?php
$app = new Application();
$app->router->get('/', function () {
return 'Hello, world';
});
$app->router->get('/contact', function () {
return 'Contact';
});
$app->run();
上記のようなコードでルーティングできるようにしたいわけです。実際に index.php
を上のコードに変更してみてください。もちろんエラーになります。実現するためには Application
クラスと Router
クラスが必要ですね。
core/Application.php
と core/Router.php
を作成しましょう。
<?php
class Application
{
public Router $router;
public function __construct()
{
$this->router = new Router();
}
public function run()
{
}
}
<?php
class Router
{
public function get($path, $callback)
{
}
}
メソッドはまだ空っぽですが、このような形のクラスになるかと思います。とりあえずリロードしてみると、
Fatal error: Uncaught Error: Class "Application" not found in C:\xampp\htdocs\PHPMVCFramework\index.php:3 Stack trace: #0 {main} thrown in C:\xampp\htdocs\PHPMVCFramework\index.php on line 3
と表示されます。当然ですね。一つずつ必要なものを require_once
してもよいのですが、クラスが増えていくたびに require_once
の記述を繰り返さなければいけなくなります。そのため、動画では autoload
を設定しています。
Composer を使用したオートローディングの設定
以下を実行しましょう。
composer init
ルートディレクトリに composer.json
が作成されました。以下のように、autoload
を付け加えてください。
{
"name": "hoge/phpmvc-framework",
"authors": [
{
"name": "piyo",
"email": "piyo@email.com"
}
],
"autoload": {
"psr-4": {
"app\\": "./"
}
},
"require": {}
}
その後、composer update
します。
composer update
これでautoload
が有効になりました。Application.php
と Router.php
で名前空間を宣言しましょう。
<?php
namespace app\core;
class Application
{
public Router $router;
public function __construct()
{
$this->router = new Router();
}
public function run()
{
}
}
<?php
namespace app\core;
class Router
{
public function get($path, $callback)
{
}
}
また、index.php
を以下のように変更します。
<?php
require_once __DIR__ . '/vendor/autoload.php';
use app\core\Application;
$app = new Application();
$app->router->get('/', function () {
return 'Hello, world';
});
$app->router->get('/contact', function () {
return 'Contact';
});
$app->run();
以下の2行を追加しました。
require_once __DIR__ . '/vendor/autoload.php';
use app\core\Application;
localhost:8080
をリロードしてみましょう。エラーを吐かなくなりましたね。
ルーティングの実装
Application
クラスの run
メソッドの中身を以下のようにします。
public function run()
{
$this->router->resolve();
}
これで、リクエストがあれば Router
クラスの resolve
メソッドを呼び出すようになりました。Router
クラスに resolve
メソッドを実装しましょう。
public function resolve()
{
echo 'Hi';
}
localhost:8080
をリロードすると、"Hi" という文字が表示されますね。
さて、ルーティングをするにはどのような URL にリクエストがあったのかを知る必要があります。そこで使用するのがサーバー変数 $_SERVER
です。これはスーパーグローバル変数と呼ばれるものです。 スクリプト全体を通してすべてのスコープで使用することができます。resolve
メソッドを以下のように変更してください。
public function resolve()
{
echo '<pre>';
var_dump($_SERVER);
echo '</pre>';
exit;
}
localhost:8080
をリロードしてみましょう。
array(26) {
["DOCUMENT_ROOT"]=>
string(31) "C:\xampp\htdocs\PHPMVCFramework"
["REMOTE_ADDR"]=>
string(3) "::1"
["REMOTE_PORT"]=>
string(5) "58450"
["SERVER_SOFTWARE"]=>
string(28) "PHP 8.0.3 Development Server"
["SERVER_PROTOCOL"]=>
string(8) "HTTP/1.1"
["SERVER_NAME"]=>
string(9) "localhost"
["SERVER_PORT"]=>
string(4) "8080"
["REQUEST_URI"]=>
string(1) "/"
["REQUEST_METHOD"]=>
string(3) "GET"
["SCRIPT_NAME"]=>
string(10) "/index.php"
["SCRIPT_FILENAME"]=>
string(41) "C:\xampp\htdocs\PHPMVCFramework\index.php"
["PHP_SELF"]=>
string(10) "/index.php"
["HTTP_HOST"]=>
string(14) "localhost:8080"
["HTTP_CONNECTION"]=>
string(10) "keep-alive"
["HTTP_CACHE_CONTROL"]=>
string(9) "max-age=0"
["HTTP_UPGRADE_INSECURE_REQUESTS"]=>
string(1) "1"
["HTTP_USER_AGENT"]=>
string(115) "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.116 Safari/537.36"
["HTTP_ACCEPT"]=>
string(135) "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
["HTTP_SEC_FETCH_SITE"]=>
string(4) "none"
["HTTP_SEC_FETCH_MODE"]=>
string(8) "navigate"
["HTTP_SEC_FETCH_USER"]=>
string(2) "?1"
["HTTP_SEC_FETCH_DEST"]=>
string(8) "document"
["HTTP_ACCEPT_ENCODING"]=>
string(17) "gzip, deflate, br"
["HTTP_ACCEPT_LANGUAGE"]=>
string(23) "ja,en-US;q=0.9,en;q=0.8"
["REQUEST_TIME_FLOAT"]=>
float(1617274442.500941)
["REQUEST_TIME"]=>
int(1617274442)
}
このようなレスポンスが返ってきます。$_SERVER
には、サーバー情報および実行時の環境情報が配列として入っています。SERVER_URI
に注目してください。
["REQUEST_URI"]=>
string(1) "/"
このキーがルーティングに使えそうですね。試しに、localhost:8080/hoge
にアクセスしてみましょう。
["REQUEST_URI"]=>
string(5) "/hoge"
いいですね。
ルーティングに必要な情報はもう1つあります。リクエストのメソッドです。これも、サーバー変数に含まれていました。
["REQUEST_METHOD"]=>
string(3) "GET"
これで準備ができました。サーバー変数を覗き、必用な情報を返してくれる Request
クラスを core
ディレクトリ配下に作成しましょう。
<?php
namespace app\core;
class Request
{
public Router $router;
public function getPath()
{
$path = $_SERVER['REQUEST_URI'] ?? '/';
$position = strpos($path, '?');
if ($position === false) {
return $path;
}
return substr($path, 0, $position);
}
public function getMethod()
{
return strtolower($_SERVER['REQUEST_METHOD']);
}
}
getPath
メソッドがリクエストのパスを返し、getMethod
メソッドがリクエストのメソッドを返します。注意点として、$_SERVER['REQUEST_URI']
にはパラメータも含んだ文字列が紐付いているため、パラメータが付いていた場合はそれを取り除いた値を返しています。
それでは Application
クラスを修正します。
<?php
namespace app\core;
class Application
{
public Router $router;
public Request $request;
public function __construct()
{
$this->request = new Request();
$this->router = new Router($this->request);
}
public function run()
{
$this->router->resolve();
}
}
変わったのはクラス request
というプロパティを持つようになったことと、コンストラクタの処理です。コンストラクタでやっていることは2つで、
- リクエストから
Request
インスタンスを作ってプロパティへ - 作った
Request
インスタンスを使って、Router
インスタンスを作ってプロパティへ
ということです。
それでは、与えられたリクエスト情報を適切に処理できるように Router
クラスを修正しましょう。
<?php
namespace app\core;
class Router
{
public Request $request;
protected array $routes = [];
public function __construct(Request $request)
{
$this->request = $request;
}
public function get($path, $callback)
{
$this->routes['get'][$path] = $callback;
}
public function resolve()
{
$path = $this->request->getPath();
$method = $this->request->getMethod();
$callback = $this->routes[$method][$path] ?? false;
if ($callback === false) {
echo "Not found";
exit;
}
echo call_user_func($callback);
}
}
それでは解説していきます。
例えば index.php
でルートを定義するとき、
$app->router->get('/', function () {
return 'Hello, world';
});
$app->router->get('/contact', function () {
return 'Contact';
});
のように、複数のルートを定義しますよね。そのため、Router
クラスはそれらのルートを記憶するために配列 routes
をプロパティに持ち ます。そして、get
メソッドや post
メソッドによって、配列に新しいルートを格納しています。ちなみに、routes
の中身を var_dump
で覗いてみると
array(1) {
["get"]=>
array(2) {
["/"]=>
object(Closure)#5 (0) {
}
["/contact"]=>
object(Closure)#6 (0) {
}
}
}
のようなネストした構造になります。なので、アクセスする際は
routes['get']['contact']
のような形でアクセスします。そして、resolve
メソッドは以下のようになっています。
public function resolve()
{
$path = $this->request->getPath();
$method = $this->request->getMethod();
$callback = $this->routes[$method][$path] ?? false;
if ($callback === false) {
echo "Not found";
exit;
}
echo call_user_func($callback);
}
getPath
と getMethod
についてはサーバー変数のところで解説した通りです。また、routes[$method][$path]
に値が入っていない場合は、そのルートを定義していないということなので、"Not found" を返しています。
それでは、localhost:8080
にアクセスしてみてください。"Hello, world" と表示されますね。次に localhost:8080/contact
にアクセスしてみてください。"Contact" と表示されます。最後に、localhost:8080/hoge
にアクセスしてみましょう。"Not found" が表示されましたか?
おめでとうございます!シンプルなルーティング機能を実装できました。しかし、まだ GET メソッドで文字列を返すだけです。 他の リクエストメソッドでの処理や、HTML を生成して返す処理が必用ですね。
とりあえず当記事の内容はここまでとします。興味が湧いた方は、ぜひ紹介した動画を見てみてください。もう一度リンクを貼っておきます。
Use PHP to Create an MVC Framework - Full Course
Discussion