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

美しいWebサービスは美しいURLに宿る(ｷﾘｯ

## 過去ログ

| スレ | 内容 |
|----|----|
| [インスパイヤされて掲示板を作りたくなった(1)](http://qiita.com/tadsan/items/cdbbb5b08591af2b110d) | PHPとComposer、あとべんりなライブラリ |
| [インスパイヤされて掲示板を作りたくなった(2)](http://qiita.com/tadsan/items/adaade6b675ac344f373) | SQLite3のスキーマ設計とTwigテンプレート |
| インスパイヤされて掲示板を作りたくなった(3) | ← イマココ |

## 新しいライブラリ

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

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

* **[zonuexe/objectsystem](https://packagist.org/packages/zonuexe/objectsystem)**: PHPのクラスに型を付けたり機能追加できるよ
* **[zonuexe/tetosql](https://packagist.org/packages/zonuexe/tetosql)**: SQLをちょっと安全に書けるPDOのラッパーだよ
* **[zonuexe/simple-routing](https://packagist.org/packages/zonuexe/simple-routing)**: シンプルお手軽なURLルーターだよ

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

これらのライブラリの説明についてはQiitaの[シンプルなルーティングがしたかった](http://qiita.com/tadsan/items/bcaa14504d0ecdd9e096)とか[private/protectedなプロパティを外部から読み込み可能にする](http://qiita.com/tadsan/items/2049243e47495764b191)みたいな記事に書いたけど、この記事でも軽く説明するので後で読んでくれると良いかも！

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

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(技術評論社)](http://gihyo.jp/magazine/wdpress/archive/2016/vol91)に書いたので、本屋さんで買ってくださいね ヾ(〃＞＜)ﾉﾞ (宣伝乙)

## 設定の読み込み

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

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

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

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

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

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

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

## URLルーティング

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

> - **板URL** http://viper.2ch.sc/news4vip/
> - **スレURL** http://viper.2ch.sc/test/read.cgi/news4vip/1455901382
> - **レスURL** http://viper.2ch.sc/test/read.cgi/news4vip/1455901382/12 (範囲指定可能)

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

-----

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

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

```sh
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
<?php
/**
 * @author    USAMI Kenta
 * @copyright 2016 USAMI Kenta
 * @license   WTFPL
 */
namespace InspireBBS;

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

```

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

### ルーターを使ってみる

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

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

```php
$url = $_SERVER['REQUEST_URI'];

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

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

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

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

```php
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`を置きます。

```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 を開いたときに板一覧が表示されるようになります。

### 本日のまとめ

説明する事項が多くて、まだあんまり掲示板っぽさが出てこない… ここからｋｓｋしていけるといいですね ヾ(〃＞＜)ﾉﾞ