Help us understand the problem. What is going on with this article?

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

この記事は、
ジーズアカデミー Advent Calendar 2019の8日目です。

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


こんにちは。
ふくしまです。

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


そもそもフレームワークとは??

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

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

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

ということで、このルーティングを実装してみようというのが今回の狙いです。
ルーティング以降も作成したいのですが、筆者が疲れたため次回以降記載します。

作る前に

このフレームワークは、以前千駄ヶ谷で行われた
[PHPでフレームワークを作ってみるハンズオン]の資料
(といっても何も配布物は無いので、自分のメモ)を参考に作成しています。

ねらい

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

対象

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

環境構築

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

リポジトリの用意

1,Gitの用意
以下コマンドラインで作成してください。
なお、名前は好きなものを利用してください。
mkdir ore2_fw && cd ore2_fw && git init

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

  • ターミナルで composer require php と実行。

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

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

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

{
    "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にアップする際に不要なモノは勝手にアップされなくなります。 必要に応じて、内容をカスタムしていただいても結構です。
.DS_Store
._.DS_Store
.php_cs.cache
.phpunit.result.cache
/.idea/
/cache/
/log/
/node_modules/
/vendor/

イメージは以下の通り。
スクリーンショット 2019-12-05 0.23.12.png

  • 保存したら、
    composer require ext-mbstring ext-intl
    をターミナルで実行しましょう。

  • composer v (validate) を実行して ./composer.json is validと出力されたら成功です。
    イメージは以下の通り。
    スクリーンショット 2019-12-05 0.24.04.png

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

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

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

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

phpinfo.php
<?php
phpinfo();
  • ターミナルで、 php -S localhost:8080 -t public/ と記入し、ビルトインサーバーを起動します。

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

イメージはこんな感じ。
スクリーンショット 2019-12-05 0.33.16.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ファイルなので、","などを忘れずに。

イメージはこんな感じ。

スクリーンショット 2019-12-06 1.11.04.png

  • 追加したら、composer vエラーがないことを確認し、 composer dump-autoload コマンドを実行してください。
  • 【public】というフォルに新しいファイル index.phpを追加してください。 このindex.phpの中に、require __DIR__ . '/../vendor/autoload.php';と記載してください。 また、先程作成したh()関数を利用して、何か記載してください。

イメージはこちら。
スクリーンショット 2019-12-06 1.14.43.png

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

スクリーンショット 2019-12-06 1.17.09.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とは??
つまり、ドメインより右っかわの部分ですね。

また、$_SERVER['REQUEST_URI'] === 'phpinfo'

の場合に、phpinfo()が表示されるようになっています。

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

最初の画面
スクリーンショット 2019-12-07 0.22.20.png

URLに/phpinfoを追加した場合
スクリーンショット 2019-12-08 3.04.41.png

URLにhogehoというパラメータを追加した場合
スクリーンショット 2019-12-08 4.49.32.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()に関しては、
こちらを参考になさってください。

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を実行する
等を確認してください。
*

イメージはこちら。
スクリーンショット 2019-12-08 10.01.52.png

もう一個イメージ
スクリーンショット 2019-12-08 10.02.05.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の8日目でしたー

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away