🌊

PHP でルーターを書いてみる

2021/04/01に公開

最近 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 を作成して、動作を確認しましょう。

index.php
<?php

echo "Hello, World";

php でサーバーを立ち上げるときは以下のコマンドを実行します。

php -S localhost:8080

image-20210401152651218

ちゃんと動作していますね。ただし、ルーターを作成していないので、localhost:8080/hogelocalhost:8080/fuga のようなリクエストに対しても、同じ "Hello, world" を返してしまいます。

しかし私達は、

index.php
<?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.phpcore/Router.php を作成しましょう。

Application.php
<?php


class Application
{
  public Router $router;

  public function __construct()
  {
    $this->router = new Router();
  }

  public function run()
  {
  }
}

Router.php
<?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 を付け加えてください。

composer.json
{
    "name": "hoge/phpmvc-framework",
    "authors": [
        {
            "name": "piyo",
            "email": "piyo@email.com"
        }
    ],
    "autoload": {
        "psr-4": {
            "app\\": "./"
        }
    },
    "require": {}
}

その後、composer update します。

composer update

これでautoload が有効になりました。Application.phpRouter.php で名前空間を宣言しましょう。

Application.php
<?php

namespace app\core;

class Application
{
  public Router $router;

  public function __construct()
  {
    $this->router = new Router();
  }

  public function run()
  {
  }
}
Router.php
<?php

namespace app\core;

class Router
{
  public function get($path, $callback)
  {
  }
}

また、index.php を以下のように変更します。

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 ディレクトリ配下に作成しましょう。

Request.php
<?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 クラスを修正します。

Application.php
<?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 クラスを修正しましょう。

Router.php
<?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);
}

getPathgetMethod についてはサーバー変数のところで解説した通りです。また、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