おれおれフレームワークを作成し、フレームワークの真髄に近づいてみる！<ルーティング>


この記事は、
[ジーズアカデミー Advent Calendar 2019](https://qiita.com/advent-calendar/2019/gsacademy?fbclid=IwAR1YZtRBMRlSRIzJgnR2B0wStiSzElJ4xVFUYqJI-CU9xKc8febsPbBuH7Y)の８日目です。

#令和元年の内にPHPでオレオレフレームワーク的な何かを作るぞ！

---

こんにちは。
[ふくしま](https://twitter.com/MasuraoProg)です。

この記事を書く今、もう令和元年が終わろうとしています。
元年が終わる前に、初めてのPHPフレームワークっぽいものを自作してみましょう！

---

##そもそもフレームワークとは？？

そもそもフレームワークとは何でしょうか。
『初めてのPHP』（オライリー・ジャパン）には以下のような記述があります。

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
一般にWeb開発を目的とするフレームワークは少なくとも次のタスクを行うーー

**ルーティング**
ユーザーがリクエストしたURLを、レスポンスを作成する役割を担う特定のメソッドや関数に変換する。
（以下略）
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

ということで、**このルーティングを実装してみよう**というのが今回の狙いです。
~~ルーティング以降も作成したいのですが、筆者が疲れたため次回以降記載します。~~
##作る前に
このフレームワークは、以前千駄ヶ谷で行われた
[PHPでフレームワークを作ってみるハンズオン]の資料
（といっても何も配布物は無いので、自分のメモ）を参考に作成しています。

##ねらい
・簡単なフレームワーク作りを通じてPHPの知識を深める。
・URLに .php が入ってるPHPと入ってないPHPの違いを捉えること（ルーティングの作成）
※あくまでも簡単な自作フレームワークの作成にとどまります。

##対象
HTML／PHP初級以上
・ある程度の関数を使える。
・クラスとかもまあまあわかる。
・簡単なアプリを作ったことがある。
※フレームワークの使用経験の有無は特に問いません
**※なお、基本的にコピペで行けるので知識なくてもできます。**

##環境構築
以下のツールを利用できるようにしてください。
・PHP7.1以上必須　※この記事では7.3利用
（既にセットアップ済みのPHPがあればそれで可）
・Composer
・Git
*必要に応じて、Heroku等アップしてください。

#リポジトリの用意
1,Gitの用意
以下コマンドラインで作成してください。
なお、名前は好きなものを利用してください。

```bash
$ mkdir ore2_fw && cd ore2_fw && git init
```

2,空のGitリポジトリをPHPプロジェクトとして初期化してください。

```bash
$ composer require php
```
composerにより、いくつかファイルがダウンロードされます。

- 作成されたcomposer.jsonに下記内容を記載してください。

※nameは、「所属/名前」と言う具合にスラッシュ'/'を利用しないとエラーが出ると思います。
何でもいいので適当に名付けてください。

```json
{
    "name": "hogehoge/ore2fr",
    "description": "this is my fream work",
    "license": "AGPL-3.0-or-later",
    "require": {
        "php": "^7.3"
    },
    "config": {
        "sort-packages": true
    }
}
```

- ".gitignore"というファイルをcomposer.jsonと同じフォルダ内に作成してください。
その.gitignoreの中に、以下を記載してください。
これでgitにアップする際に不要なモノは勝手にアップされなくなります。
必要に応じて、内容をカスタムしていただいても結構です。
※その他、gitignoreに関しての詳細は各自でググってください。

```text
.DS_Store
._.DS_Store
.php_cs.cache
.phpunit.result.cache
/.idea/
/cache/
/log/
/node_modules/
/vendor/
```

イメージは以下の通り。
<img width="595" alt="スクリーンショット 2019-12-05 0.23.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/442f27ef-8221-60ce-0f3a-0fe28f6c9a27.png">

- 保存したら

```bash
$ composer require ext-mbstring ext-intl
```
をターミナルで実行しましょう。

- `composer v` (validate) を実行して `./composer.json is valid`と出力されたら成功です。
イメージは以下の通り。
<img width="568" alt="スクリーンショット 2019-12-05 0.24.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/1bb99c6a-71c0-f2cf-d820-72c91fb72c6c.png">

ココまで来たら、適度に、
`git add -A`で生成されたファイルを追加し `git commit -m 'お好きなメッセージ' しましょう。

3,ドキュメントルートを用意しましょう。
※ドキュメントルートとは、外部に公開するファイルなどが置かれたフォルダ/ディレクトリのことです。

- とりあえず【public】というフォルダを用意してください。

- publicフォルダの中に、`phpinfo.php`というファイルを用意してください。
中身は、以下のとおりです。

```php:phpinfo.php
<?php
phpinfo();
```

- ターミナルで、

```bash
php -S localhost:8080 -t public/
```
と記入し、ビルトインサーバーを起動します。

その後、[http://localhost:8080/phpinfo.php](http://localhost:8080/phpinfo.php ) を開いて表示されたら成功です。

イメージはこんな感じ。
<img width="1384" alt="スクリーンショット 2019-12-05 0.33.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/adfc4fc5-5674-6a13-c692-a885be988d06.png">

表示を確認したら、サーバーを立ち上げているターミナルにて
`Control+c`でサーバーを終了させます。

4,関数とファイルを追加する
- srcというフォルダを用意してください。
- その中に、`functions.php`を用意。
このfunctions.phpの中に下記記載してください。
XSSを防ぐための、htmlspecialcharsの用意です。

```functions.php
function h($s) {
  return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
```

- `composer.json`に、下記内容を追加してください。
`"autoload": {"files": ["src/functions.php"]}`
場所はどこでもいいです。

※jsonファイルなので、","などを忘れずに。

イメージはこんな感じ。
<img width="459" alt="スクリーンショット 2019-12-06 1.11.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/803a2953-63c4-be7c-6541-d5c7c3724320.png">
---
- 追加したら、`composer v`エラーがないことを確認し、 `composer dump-autoload` コマンドを実行してください。
- 【public】というフォルダに新しいファイル `index.php`を追加してください。
このindex.phpの中に、`require __DIR__ . '/../vendor/autoload.php';`と記載してください。
また、先程作成した`h()`関数を利用して、何か記載してください。

イメージはこちら。
<img width="478" alt="スクリーンショット 2019-12-06 1.14.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/e9de7521-1ec4-f4d4-bd19-a8d1bc2217bc.png">

- またビルトインサーバを利用してください。
ターミナルで、
`php -S localhost:8080 -t public/`
と記入。[http://localhost:8080](http://localhost:8080) を開いて表示されたら成功です。
イメージはこちら。

<img width="334" alt="スクリーンショット 2019-12-06 1.17.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/81d24d87-d885-0406-ebe5-0e3f70aca5b0.png">

autoload.phpにより、関数ファイルが自動的に読み込まれていることが確認できましたね。

#ルーティングの作成
さて改めまして、フレームワークのいいところに、ルーティングというものがあります。

通常、PHPでウェブページを作成すると、URLの最後に
`https:// xxxxxxxxxx .php`
って、最後に.phpってつくのが通常ですね。
ルーティングとはURLのクエリパラメーターに、特定の文字列が追加された場合、
その文字列に合致したページを表してくれることです。

たとえば、
/member　だったら、会員ページを示したり、
/about　だったら、その会社の基本情報を表示してくれたり。
よくわからんというひとは、まずやってみましょう！！！

---
1,ルーティング準備
まず`index.php` を以下のように書き換えましょう。

```index.php

<?php
require __DIR__ . '/../vendor/autoload.php';

$routes = [];
$routes['/'] = function(){
    if ($_SERVER['REQUEST_URI'] === '/') {
        echo "<!DOCTYPE html>\n";
        echo "<title>test</title>\n";
        echo "<p>現在は" . h(date('Y年m月d日H時i分s秒')). "です</p>\n";
        echo "<ul><li><a href='/phpinfo'><code>phpinfo()</code></a></ul>\n";
        exit;
    }
};

$routes['/phpinfo'] = function(){
    phpinfo(); 
};

if (isset($routes[$_SERVER['REQUEST_URI']])) {
    $f = $routes[$_SERVER['REQUEST_URI']];
    $f();
} else {
    http_response_code(404);
    echo "<h1>404だぞ！</h1>";
}
```

---
記載したとおり、`$_SERVER['REQUEST_URI'] === '/'`の場合に、トップページが
表示されるようにしました。
[$_SERVERとは??](https://www.php.net/manual/ja/reserved.variables.server.php)
つまり、ドメインより右っかわの部分ですね。

また、`$_SERVER['REQUEST_URI'] === 'phpinfo'`
の場合に、`phpinfo()`が表示されるようになっています。

---

- では、ターミナルで、
`php -S localhost:8080 -t public/`
と記入し、ビルトインサーバーを起動します。

*最初の画面*
<img width="462" alt="スクリーンショット 2019-12-07 0.22.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/870bc12c-c301-d987-060c-6910a0dea397.png">

*URLに`/phpinfo`を追加した場合*
<img width="1270" alt="スクリーンショット 2019-12-08 3.04.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/d1f4a879-4437-05ef-c961-9f9ada189912.png">

*URLに`hogeho`というパラメータを追加した場合*
<img width="432" alt="スクリーンショット 2019-12-08 4.49.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/dac0fd51-e614-af40-3403-befbafb71803.png">

もし問題なければ、phpinfo.phpは削除していただいて結構です。

----

##各フォルダに機能ごとに分ける。

せっかくルーティング機能を作ったので、
- ルーティングするファイル
- 表示内容をまとめたファイル
などに分けましょう。

1,まず、appというフォルダを新規作成してください。
2,appフォルダの中に、viewというフォルダを用意してください。
3,viewフォルダの中に、`index.php`を用意

`index.php`の中身は以下のとおりです。
※トップページに表示する内容になります。

```app/veiw/index.php
<!DOCTYPE html>
<title>test</title>
<p>現在は <?= h(date('Y年m月d日H時i分s秒')); ?> です </p>
<ul><li><a href='/phpinfo'><code>phpinfo()</code></a></ul>
```

4,次に、appフォルダの中に、routes.phpを用意。

```routes.php
<?php
$routes = [];

$routes['/'] = function(){
   include __DIR__ . '/view/index.php';
};

$routes['/phpinfo'] = function(){
    phpinfo(); 
};
return $routes; 
```

5,`public/index.php`の中身を書き換える。
以下のように書き換えてください。

```public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';

$routes = require __DIR__ .'/../app/routes.php';
$request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if (isset($routes[$_SERVER['REQUEST_URI']])) {
    $f = $routes[$_SERVER['REQUEST_URI']];
    $f();
} else {
    http_response_code(404);
    echo "<h1>404だぞ！</h1>";
}

```
今回の書き換えで行われたことは、、、

・まず、public/index.phpでurlを確認。
　その際に、requireにより、$routesを呼び出し、URLの内容によって、ルーティング。
　もし、URLが”/”であれば、`view/index.php'`を呼び出し、表示。
　”/phpinfo”であれば、`phpinfo(); `を表示。
　それ以外であれば、自分で設定した　404の表示がされます。

　なお、今回の書き換え（
　クエリパラメータに何か記載されても問題なく処理されていると思います。
　※クエリパラメータ…GETで送るときに?でURLにくっついてるあれ。
``$request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);``らへんを追加することで、この処理を行っています。

これらの処理により、
- `routes.php`に指定したURLを指定。その際に表示させたいファイルを`include`
- `include`したファイルに、表示させたい内容を記載する
とすれば、ルーティングができると思います。

ということで、なんとなく
**ルーティング処理ができました！！！！**

##中身を書き換える
少し中身を整理しましょう。
それぞれ、以下の通り書き換えてください。

```app/routes.php
<?php
$routes = [];
$routes['/'] = function(){
    ob_start();
    include __DIR__ . '/view/index.php';
    return [200, ['Content-Type' => 'test/html'] , ob_get_clean()];
};

$routes['/phpinfo'] = function(){
    ob_start();
    phpinfo(); 
};

return $routes; 
```

ob_start()に関しては、
[こちら](https://blog.s-giken.net/249.html)を参考になさってください。


```public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';

$routes = require __DIR__ .'/../app/routes.php';
$request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

$not_found = function (){
    return [404, ['Contentd-Type' => 'text/html'],"<h1>404 NotFound だよ</h1>"];
};

//URLに何かあれば$fに代入、なければ$not_found
//$aa = $bb ?? $cc; については、「Null 合体演算子」で検索
$f = $routes[$request_uri] ?? $not_found;


//returnされた[xxx, ['Contentd-Type' => 'xxx'],"xxx"]を代入
[$status, $headers, $body] = $f();
http_response_code($status);
foreach($headers as $name => $h){
    header("{name}:$h");
}cho $body;
```
確認してみましょう。
1,public/index.phpにて`$request_uri`を確認
※`$request_uri`は、URLのgoogle.com/hogehogeのhogehoge部分。
2,もし、`$request_uri`があれば`routes.php`の、
[HTTPステータスコード, Content-Type, body]の情報を取得して表示。
3,もし`$request_uri`がなければ、404を表示。
となっています。

##クラスを加えて、色々できるようにする
やはり、フレームワークということで、
ルーティングクラスを作りましょう。

1,`src/Template.php`を作成し、以下コピペしましょう。

```src/Template.php
<?php
namespace Oira;
class Template
{
    /** @var string ファイルパス */
    private $file;
    /** @var array テンプレートで表示するための変数を保持した配列 */
    private $params;
    public function __construct($file, array $params)
    {
        $this->file = $file;
        $this->params = $params;
    }

    public function render()
    {
        extract($this->params);
        include $this->file;
    }
    
    public function __toString()
    {
        ob_start();
        $this->render();
        return ob_get_clean();
    }
}
```

2,`src/TemplateFactory.php`を作成して以下コピペ

```src/TemplateFactory.php
<?php
namespace Oira;
class TemplateFactory
{
    /** @var string テンプレートのベースディレクトリ */
    private $base_dir;
    public function __construct(string $base_dir)
    {
        $this->base_dir = rtrim($base_dir, '\\/');
    }
    
    public function create(string $name, array $params): Template
    {
        return new Template("{$this->base_dir}/{$name}.php", $params);
    }
}
```

3,`composer.json`に以下追加
**※上下の記載は省略してます。**

```composer.json
    "autoload": {
        "psr-4": {
            "Oira\\": "src/"
        }
```
4,`app/routes.php`を以下のように書き換える

```app/routes.php
<?php
$routes = [];
$template = new \Oira\TemplateFactory(__DIR__ . '/view/');

$routes['/'] = function () use ($template) {
    return [200, ['Content-Type' => 'text/html'], 
    $template->create('index', [
        'name' => 'てすとまん',
        'route' => 'ルートからのテスト'
    ])];
};
$routes['/phpinfo'] = function(){
    ob_start();
    phpinfo(); 
};
return $routes;
```

5,`app/view/index.php`に以下のように追加

```app/view/index.php
<!DOCTYPE html>
<title>test</title>
<p>現在は <?= h(date('Y年m月d日H時i分s秒')); ?> です </p>
<p><?= h($name) ?></p>
<p><?= h($route) ?></p>
<ul><li><a href='/phpinfo'><code>phpinfo()</code></a></ul>
```

上記対応後、ローカルホストで内容を確認してみてください。
**※autoloadでclassが読み込めない場合は、
1,ディレクトリ名の大文字小文字等を一致させる
2,`composer.json`に`{"psr-4": {"Myapp\\": "src/myapp"}}`が記載あるか確認する
3,`composer dump-autoload --optimize`を実行する
等を確認してください。
**

イメージはこちら。
<img width="411" alt="スクリーンショット 2019-12-08 10.01.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/48f2b1b0-f618-cf1c-bddc-8dd937c6c792.png">

もう一個イメージ
<img width="1056" alt="スクリーンショット 2019-12-08 10.02.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/9c73f73e-08e1-d424-ce10-cce8b2a91a78.png">

**今回行ったこと。**

- `app/routes.php`の

```app/routes.php
'index', [
        'name' => 'てすとまん',
        'route' => 'ルートからのテスト'
    ]
```
の部分に記載したkeyとvalueを確認してください。
このkey部分を`app/view/index.php`内に、`<?= h($name) ?>`
と記載することで、実際に表示されているのがわかります。
新たに追加した`TemplateFactory`等を通じて、
$paramsとしてcreateメソッドを通じ、このように表示されています。

---


フレームワークでは重要なルーティングとビューに関わる部分を作成してみました。
これにより、ルーティングという作業で何が行われているのかを確認できたかと思います。
続きは、頑張れれば書きたいと思います。


以上、[ジーズアカデミー Advent Calendar 2019](https://qiita.com/advent-calendar/2019/gsacademy?fbclid=IwAR1YZtRBMRlSRIzJgnR2B0wStiSzElJ4xVFUYqJI-CU9xKc8febsPbBuH7Y)の８日目でしたー