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

  • 10
    Like
  • 3
    Comment
More than 1 year has passed since last update.

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

おことわり

  • 筆者は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 (範囲指定可能)

ドメインは割とどうでも良くて、news4vip145590138212ってあたりが大事ですね。なので、これらのIDは常にインデックスを張っておきます。実は——ともったいつけるほどのことでもないけど、2ちゃんねるのスレッドIDは常にエポックタイムです。

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

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

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

CREATE TABLE文を書いてみる

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

前回のファイルの配置表に書いてないけど、tests/db/init.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にだけAUTOINCREMENTidがついてるのは投稿単位であぼーんしやすくするためですね… 伝統的な2ちゃんねるの仕様では「スレは板ごとに一秒に一つ」のルールがあるけど、レスには特にそのような制約がない1

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

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

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

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

% 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は躊躇せずに削除します。

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

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

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文もやっぱりコマンドで読み込みます。

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}.sqlsrc/View/templateのあたりが増えてます。

なぜTwigか

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

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

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

Twigの文法はTwig for Template Designersに載ってるので、困ったら目を通すといいです。Emacsを使ってるひとはweb-mode.el - html template editing for emacsとか使ってみるとべんりです。

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

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の準備

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

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

元気に挨拶をしよう

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

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

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

    return "こんばんわ";
}

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

今回の記事の締めです。

// $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の話も突っ込んだら意外と長くなっちゃった気がする… 明日は掲示板を動かすところまで進めればイイナ ヾ(〃><)ノ゙☆

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


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