インスパイヤされて掲示板を作りたくなった(3)

  • 19
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

美しいWebサービスは美しいURLに宿る(キリッ

過去ログ

スレ 内容
インスパイヤされて掲示板を作りたくなった(1) PHPとComposer、あとべんりなライブラリ
インスパイヤされて掲示板を作りたくなった(2) SQLite3のスキーマ設計とTwigテンプレート
インスパイヤされて掲示板を作りたくなった(3) ← イマココ

新しいライブラリ

今回の記事で利用するライブラリをインストールしよう。

composer require zonuexe/simple-routing zonuexe/objectsystem zonuexe/tetosql

いちおう、 どれも自作(または自分でメンテナンスしてるやつ)だよ!

これらのライブラリの説明についてはQiitaのシンプルなルーティングがしたかったとかprivate/protectedなプロパティを外部から読み込み可能にするみたいな記事に書いたけど、この記事でも軽く説明するので後で読んでくれると良いかも!

ファイルの読み込みについて

Composerはいかしたライブラリの依存管理ツールですが、プロジェクト内の管理ファイルを超簡単に読み込む機能が付いてます。この機能のおかげで、基本的にはinclude_onceでファイルを読み込む必要はほとんどなくなりました。

composer.json"autoload"ってところに適当に追加してやるだけです。

composer.json
{
    "autoload": {
        "files": ["src/functions.php"],
        "psr-4": {
            "InspireBBS\\": "src/"
        }
    },
    "require": {
        "vlucas/phpdotenv": "^2.2",
        "twig/twig": "^1.24",
        "twig/extensions": "^1.3",
        "zonuexe/simple-routing": "^0.5.0",
        "zonuexe/objectsystem": "^0.5.3",
        "zonuexe/tetosql": "^0.0.1"
    },
    "require-dev": {
        "filp/whoops": "^2.0"
    }
}

"files"に配列で指定されたファイルは、初期化時に必ず読み込まれるようになります。"psr-4"に名前空間とディレクトリを指定してやると、クラスが必要になったときにPHPが自動で読み込んでくれるように設定してくれます。

オートローディング機能の詳細な紹介は、近々発売するWEB+DB PRESS Vol.91(技術評論社)に書いたので、本屋さんで買ってくださいね ヾ(〃><)ノ゙ (宣伝乙)

設定の読み込み

理想的にはPHPに直接書き込みたくない設定情報があります。設定を読み込むためにvlucas/phpdotenvを利用します。

index.phpの良さげな位置に次のようなコードを追加してやってください。

index.php
$dotenv = new \Dotenv\Dotenv(dirname(__DIR__));
$dotenv->overload();
$dotenv->required('DB_DSN')->notEmpty();

こうすることで、設定ファイルの読み込みと、必須設定項目のエラーチェックができます。このコードではDB_DSNの設定を必須とします。

プロジェクトルートの.envファイルに次のような感じで書いてやります。

.env
DB_DSN = "sqlite:/your/path-to/inspire-bbs/cache/db.sq3"

この設定情報はsrc/functions.phpに定義したdb()の中で利用します。

URLルーティング

前回の記事で、URLの設計の参考として2chのURLを列挙しました。

PHPでWebサービスを設計するには、どうすれば良いのでしょうか?


ところで今まで http://localhost:3939/ のようなURLで動作チェックしてましたね。では今度は http://localhost:3939/qawsedrftgyhujikolp を開いてみましょう。同じ内容が表示されたのではないでしょうか。

改めてPHPのビルトインサーバーを起動するコマンドを確認してみましょう。

php -S localhost:3939 public/index.php

public/index.phpを指定してますね。これは、どのURLにリクエストされてもindex.phpだけを実行する、の意味です。

これは不便でしょうか? いいえ、PHPで実行を分岐してやれば好きな画面を表示してやることができます。

これ検証するには、index.phpを書き換へてみませう。全部消すのはめんどくさいので、namespaceの次の行あたりにvar_dump($_SERVER['REQUEST_URI']);exit;とか適当に書いてexitするのがいいです。

<?php
/**
 * @author    USAMI Kenta
 * @copyright 2016 USAMI Kenta
 * @license   WTFPL
 */
namespace InspireBBS;

var_dump($_SERVER['REQUEST_URI']);exit;

こんな感じ。この状態でいろいろなURLを開いてみるといいですね。満足したら戻すべし。

ルーターを使ってみる

「ルーター」とは多義的な言葉ですが、Webアプリケーションの文脈でのルーティングとは、HTTPリクエスト(特にURLとメソッドに注目)を元に実行する処理を選び分ける機能をルーターと呼ぶことがあります。

何も考へずに実装すると、URLごとに表示を変更する処理はこうなりますよね。

$url = $_SERVER['REQUEST_URI'];

if ($url === '/') {
    echo "トップページだよ";
} else ($url === '/list') {
    echo "板一覧だよ";
} else {
    echo "このページは存在しません";
}
exit;

でも、これに複雑な分岐を追加するのはめんどくさそうだよね? 実際めんどくさい。

ので、ルーティング用のライブラリを使ってみます。巷ではnikic/FastRouteが人気だけど、私はあまり好きじゃないので自作のzonuexe/simple-routingを利用します。その代りに性能はあんまり良くないので、気になるなら自分でFastRouteを調べてみてね。

zonuexe/simple-routingはHTTPメソッドとURL(path)をもとに特定の値を返す、といふ役割だけを持ちます。「特定の値」とは何でも良いのですが、今回はクロージャを返すことにしてみます。

use Teto\Routing\Action;

// この配列にHTTPメソッド、URLと返したい値(クロージャ)を追加してしていくよ
$routing_map = [];
// クロージャは、Actionを受け取って表示内容を文字列で返す
$routing_map[] = ['GET', '/', function (Action $action) use ($twig, $now) {
    return $twig->render('index.tpl.html', [
        'greeting' => greeting($now),
    ]);
}];

$routing_map[] = ['GET', '/list', function (Action $action) use ($twig, $now) {
    // あとでちゃんと実装する
    return $twig->render('list.tpl.html', []);
}];

// ...
// このへんにいろんな機能を追加していくよ
// ...

// どのページにもマッチしなかったときに表示するやつ
$routing_map['#404'] = function (Action $action) use ($twig) {
    return "そんなページありませんよ…";
};

// ルーターを起動する
$router = new \Teto\Routing\Router($routing_map);
// Actionオブジェクトにはいろんな情報が詰まってるよ
$action = $router->match($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);

// クロージャを実行してみる
$content = call_user_func($action->value, $action);

// このあたりは前回までとおんなじ
header('Content-Type: text/html; charset=utf-8');
header('Content-Length: ' . strlen($content));
echo $content;

モデルを定義してみる

モデルと呼ばれる層がどのような役割を持つべきかは常に議論を呼ぶところですが、今回の目的はお手軽に実装することなので、いくつかの責務を合せ持たせることにします。

  • モデルはSQLクエリを実行し、DBからデータを取り出します
  • モデルのインスタンスはプロパティとして、DBのカラムに対応する値を持ちます
  • モデルはDBから取り出されたデータをインスタンスにマッピングする機能を持ちます
  • Rails/ActiveRecordのアソシエーションのような機能は用意しません

この判断に是非はありましょうが、これで困るまではこのまま行きます…

Boardモデルを作成する

src/Modelディレクトリを作成し、その中にBoard.phpを置きます。

Board.php
<?php
namespace InspireBBS\Model;
use Teto\SQL;

/**
 * 板を表現するモデル
 *
 * @author    USAMI Kenta
 * @copyright 2016 USAMI Kenta
 * @license   WTFPL
 */
final class Board
{
    use \Teto\Object\TypedProperty;

    private static $property_types = [
        'id'    => 'string',
        'name'  => 'string',
        'text'  => 'string',
    ];

    public function __construct(array $properties)
    {
        $this->setProperties($properties);
    }

    public function setProperties(array $properties)
    {
        if (isset($properties['id'])) {
            $this->id   = $properties['id'];
        }
        if (isset($properties['name'])) {
            $this->name = $properties['name'];
        }
        if (isset($properties['text'])) {
            $this->text = $properties['text'];
        }
    }

    /**
     * @return Board[]
     */
    public static function findAll()
    {
        $data = SQL\Query::execute(db(), self::findAll_query, [])
            ->fetchAll(\PDO::FETCH_ASSOC) ?: [];

        $boards = [];
        foreach ($data as $b) {
            $boards[] = new Board($b);
        }

        return $boards;
    }
    const findAll_query = '
        SELECT `id`, `name`, `text` FROM `boards`
    ';
}

今後も拡張していくことになりますが、こんな感じになります。各コードの意味は次回にきちんと解説します。

index.php
$routing_map[] = ['GET', '/list', function (Action $action) use ($twig, $now) {
    return $twig->render('list.tpl.html', [
        'boards' => Model\Board::findAll(),
    ]);
}];

↑ みたいな感じで追加してやれば http://localhost:3939/list を開いたときに板一覧が表示されるようになります。

本日のまとめ

説明する事項が多くて、まだあんまり掲示板っぽさが出てこない… ここからkskしていけるといいですね ヾ(〃><)ノ゙