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

前スレ: [インスパイヤされて掲示板を作りたくなった(1)](http://qiita.com/tadsan/items/cdbbb5b08591af2b110d)

> **おことわり**：
>
> - *筆者はSQLite素人だから、ぐぐりながらこの記事を書いてるよ*
> - *高トラフィックなサービス向けには設計してないよ*

## DBスキーマを定義しよう

次の一歩としてどこから手をつけようかと悩んだけど、まあDBから定義してみよう。~~苦手なんだよね…~~

DBを定義するってことは、どんなデータが必要か、どんな値をキーに引き出されるのかを想像する必要がある。

今回は2ちゃんねるをぱくる必要があるので、2ちゃんねるを研究すれば良い。とはいっても良く知ったサービスだからいまさら詳しく調べるまでもなくはあるけど、まとめてみよう。

今回はニュース速報VIPを例にしてみる。

- **板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` (範囲指定可能)

ドメインは割とどうでも良くて、`news4vip`と`1455901382`と`12`ってあたりが大事ですね。なので、これらのIDは常にインデックスを張っておきます。実は——ともったいつけるほどのことでもないけど、2ちゃんねるのスレッドIDは常に[エポックタイム](https://ja.wikipedia.org/wiki/UNIX%E6%99%82%E9%96%93)です。

さてDB設計について。今回の元ねたになった掲示板実装では`dbname`としてDBを別ファイルに分けてたっぽいけど、特にメリットはなさそうなのでSQLite3のDBファイルは1個にしておきます。

あと板ごとにテーブル増やすとかもめんどくさいので却下します。投稿は全部`posts`テーブルに入れます。

筆者はSQLもSQLiteも素人なので、自分で実装するときは間に受けないで自分で判断してみてね (´･ω･`)

### `CREATE TABLE`文を書いてみる

SQLiteにつっこむための`CREATE TABLE`を書いてみましょうか。

前回のファイルの配置表に書いてないけど、`tests/db/init.sql`とかに書いておくことにしましょうか。

```sql:tests/db/init.sql
-- -*- sql-product: sqlite -*-

CREATE TABLE `boards`( -- 板
    `id`   TEXT PRIMARY KEY, -- 板ID(slug)
    `name` TEXT NOT NULL,    -- 板名
    `text` TEXT NOT NULL     -- 説明文
);

CREATE TABLE `threads`( -- スレ
    `timestamp` INT  NOT NULL, -- スレID(timestamp)
    `board_id`  TEXT NOT NULL, -- 板ID(slug)
    `title`     TEXT NOT NULL, -- スレタイ
    PRIMARY KEY( `timestamp`, `board_id` )
);

CREATE TABLE `posts`( -- レス
    `id`               INTEGER PRIMARY KEY AUTOINCREMENT,
    `board_id`         TEXT NOT NULL, -- timestamp
    `thread_timestamp` INT  NOT NULL, -- スレID(timestamp)
    `posted_at`        TEXT NOT NULL, -- 日付 (Y-m-d H:i:s)
    `name`             TEXT NOT NULL, -- 名前(+トリップ)
    `email`            TEXT NOT NULL, -- e-mail (または、キャップ)
    `author_hash`      TEXT NOT NULL, -- 投稿者ID
    `message`          TEXT NOT NULL, -- 投稿される本文
    `ip_addr`          TEXT NOT NULL  -- IPアドレス、京都府警から令状が届いたら困るよね
);
CREATE INDEX `post_board_thread` ON `posts`( `board_id`, `thread_timestamp` );
CREATE INDEX `post_author_hash` ON `posts`( `author_hash` ); -- 必死チェッカー
```

最初の行はEmacsにSQLiteだと認識させるための魔法の呪文だよ。ほかのエディタは知らんがな。

方針としては

- テーブル名は英単語の複数形
- カラム名は略さない
- `NOT NULL`制約をつける

`posts`にだけ`AUTOINCREMENT`な`id`がついてるのは投稿単位であぼーんしやすくするためですね… 伝統的な2ちゃんねるの仕様では「スレは板ごとに一秒に一つ」のルールがあるけど、レスには特にそのような制約がない[^1]。

このIDは、いはゆる「レス番」とは別です。それは別の手段で管理できるので。。。

このデータをつっこむには`sqlite3`コマンドとかでいける気がする。

```sh 
sqlite3 ./cache/db.sq3 < tests/db/init.sql
```

エラーがなければ特に何も表示されません。便りのないのは良い知らせ、って名せりふを知らないのかよ。

```sh
% sqlite3 ./cache/db.sq3 < tests/db/init.sql
Error: near line 16: AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY
Error: near line 27: no such table: main.posts
Error: near line 28: no such table: main.posts
```

文法エラーがあったらきちんと表示される。そうそう、私は何かあったら`./cache/db.sq3`は躊躇せずに削除します。

[^1]: 1秒に一人しか投稿できない掲示板とか嫌だろ、常識的に考へて…

### シードデータをつっこんでみる

seed(種)ってRails用語なのか知らないけど、ここは開発用の初期化データのこと。

```sql:tests/db/init.sql
-- -*- sql-product: sqlite -*-

-- 板
INSERT INTO `boards` VALUES("gline", "ガイドライン＠インスパイヤー", "ここがガ板ですよ。。。" );

-- スレ
INSERT INTO `threads` VALUES(123456789, "gline", "インスパイヤのガイドライン" );

-- 投稿
INSERT INTO `posts`( `board_id`, `thread_timestamp`, `posted_at`, `name`, `email`, `author_hash`, `message`, `ip_addr` )
    VALUES("gline", 123456789, "2016-02-17 04:04:04", "たっどさん", "#いんすぱいやー", "tW1nDri11" ,"てすとです…", "127.0.0.1");

INSERT INTO `posts`( `board_id`, `thread_timestamp`, `posted_at`, `name`, `email`, `author_hash`, `message`, `ip_addr` )
    VALUES("gline", 123456789, "2016-02-18 04:04:04", "たっどさん", "#いんすぱいやー", "tW1nDri11", "もういい加減ねるぽ", "127.0.0.1");
```

もっと長いやつ https://github.com/zonuexe/inspire-bbs/blob/master/tests/db/seed.sql

このSQL文もやっぱりコマンドで読み込みます。

```sh 
sqlite3 ./cache/db.sq3 < tests/db/seed.sql
```

エラーが出なければ、こんな感じで良いのでは。

## Twigを使ってみる

もうSQLの説明を書いてるだけで疲れたけど、せっかくなので前回インストールしたTwigでページ表示をやってみますか。

### ディレクトリ構成

前回の内容からちょっとだけ追記したディレクトリ構成を載せておきます。

```
% tree
.
├── README.md
├── cache
│   ├── db.sq3
│   └── twig
├── composer.json
├── composer.lock
├── public
│   └── index.php
├── src
│   ├── View
│   │   └── template
│   │       ├── base.tpl.html
│   │       └── index.tpl.html
│   └── functions.php
├── tests
│   └── db
│       ├── init.sql
│       └── seed.sql
└── vendor
```

`tests/db/{init,seed}.sql`と`src/View/template`のあたりが増えてます。

### なぜTwigか

[Twig](http://twig.sensiolabs.org/)はテンプレートエンジンです。「PHPってテンプレートエンジンじゃないの？」と思ってる型がいらっしゃるといけないのだけれど、PHPをテンプレートエンジンにすると値のエスケープなどがめんどくさくて、セキュリティを保障することが難しくなります。

Twigは導入実績も非常に多く、<q>The flexible, fast, and secure template engine for PHP</q>と謳ってるだけあって一般的な用途では生のPHPよりも危険なコードを埋め込みにくくなってます。*(安全になるとは言ってない)*

デメリットを言っておくと、PHPとは別の文法を覚えなければいけないところですかね…

Twigの文法は[Twig for Template Designers](http://twig.sensiolabs.org/doc/templates.html)に載ってるので、困ったら目を通すといいです。Emacsを使ってるひとは[web-mode.el - html template editing for emacs](http://web-mode.org/)とか使ってみるとべんりです。

### 共通テンプレートを用意する

Twigのいいところは、**テンプレート継承**の概念があるところですね。

まづはシンプルな共通テンプレートを用意してみましょう。このテンプレートは、ほとんど全ページから継承します。

```src/View/template/base.tpl.html
<!DOCTYPE html>
<html lang="ja">
<title>{% block title %}いんすぱいやーBBS{% endblock %}</title>
<body>
{% block content %}{% endblock %}
{% block footer %}
<hr>
<footer>
    <address>
        &copy; Copyright 2016 USAMI Kenta<br>
        <a href="https://github.com/zonuexe/inspire_bbs">InspireBBS</a> is licenced under <a href="http://www.wtfpl.net/"><abbr title="Do What the Fuck You Want to Public License">WTFPL</abbr></a> and/or <a href="http://www.gnu.org/licenses/agpl-3.0.html"><abbr title="GNU Affero General Public License, Version 3">AGPLv3</a>
    </address>
</footer>
{% endblock %}
</html>
```

`{% block xxxxx %}…{% endblock %}`みたいな構文がみそで、この部分は継承したテンプレートで上書きできます。この例だと、`{% block title %}いんすぱいやーBBS{% endblock %}`は、継承したテンプレートで上書きしなければ「いんすぱいやーBBS」が出力されます。

`{% block content %}{% endblock %}`には、ページごとのメインコンテンツが入る想定です。

### 個別ページを作る

今度は`base.tpl.html`を継承したページを作ります。さしあたって今回はトップページを`index.tpl.html`って名前にします。

```src/View/template/index.tpl.html
{% extends "base.tpl.html" %}
{% block title %}いんすぱいやーBBSへようこそ☆{% endblock %}
{% block content %}
<h1>いんすぱいやーBBS</h1>

<p>ヾ(〃＞＜)ﾉﾞ＜ {{ greeting }}</p>
{% endblock %}
```

- `{% extends "base.tpl.html" %}`と書くことで、`base.tpl.html`を継承
- `{% block content %}…{% endblock %}`の中身にHTMLをがしがしと書いていく
- `{{ greeting }}`は変数を出力する (PHPと違って`$`無し)

雑な説明だけど、こんな感じでよろしいですかね。変数については後で説明します。

### Twigの準備

```php
$basedir = dirname(__DIR__);
$loader = new \Twig_Loader_Filesystem($basedir . '/src/View/template');
$twig   = new \Twig_Environment($loader, [
    'cache' => $basedir . '/cache/twig',
    'debug' => true,
]);
```

こんな感じに書くとTwigの初期化ができます。ディレクトリから空気を読んでテンプレートを見付けてほしいので`Twig_Loader_Filesystem`を利用。あと、今はデバッグしか想定しないので`'debug' => true`にしてます。


### タイムゾーンの定義

Webサービスと時間帯の問題は実に実に根深くめんどくさい問題があるのだけれど、まあ今回は趣味の適当なサービスなので実に適当なことにしておきます。

```php
// 日本時間にセットしておく
date_default_timezone_set('Asia/Tokyo');
// 現在時刻のオブジェクト
$now = new \DateTimeImmutable;
```

### 元気に挨拶をしよう

せっかくなので挨拶をしたいですね。

```php
/**
 * @return string
 */
function greeting(\DateTimeInterface $dt)
{
    $hour = (int)$dt->format('H');

    if (4 <= $hour && $hour < 10) {
        return "お早うございます";
    }
    if (10 <= $hour && $hour < 17) {
        return "こんにちは";
    }

    return "こんばんわ";
}
```

### テンプレートを出力してみる

今回の記事の締めです。

```php
// $twig->render()でテンプレートを文字列に出力する
// パラメータとして連想配列を渡してやると、テンプレート内で変数として利用できる
$content = $twig->render('index.tpl.html', [
    'greeting' => greeting($now),
]);

header('Content-Type: text/html; charset=utf-8');
header('Content-Length: ' . strlen($content));
echo $content;
```

`$twig->render()`を読んだだけじゃ画面に出力はされないので、`echo $content;`って書いてやります。その間にHTTPのレスポンスヘッダを定義してやります。いいですね?

## 本日のまとめ

DBの話だけじゃ物足りないよね、と思ってTwigの話も突っ込んだら意外と長くなっちゃった気がする… 明日は掲示板を動かすところまで進めればイイナ ヾ(〃＞＜)ﾉﾞ☆

何か質問があったらコメントください。