【ドットインストール】PHPで作る投票システムの解説



![完成図：投票システム](https://qiita-image-store.s3.amazonaws.com/0/62881/afd28ab1-80c8-97ff-4e5d-118043f8928c.gif "voting_system000.gif")


[ドットインストール](http://dotinstall.com/)のレッスン、「[PHPで作る投票システム (全13回)](http://dotinstall.com/lessons/poll_php_v2)」の備忘録です。復習用にお使いください。

最後のGoogle Chart Toolsについては触れておりません。ドットインストールの方でレッスンが用意されています。覚えることでもないので、さっと確認して使えます。

[Google Chart Tools入門 (全12回)](http://dotinstall.com/lessons/basic_chart_tools)

デベロッパーサイトの英語が読めるようになったら、Google Chart Toolsに関しての記事を書きたいなと思っています。

<!--more-->

### USE文とユニークキー #02

[\#02 データベースの設定をしよう](http://dotinstall.com/lessons/poll_php_v2/9702)

**GRANT文**

GRANT文の基本書式は次の通り。

```mysql
GRANT '権限' ON 'データベース名.テーブルト名' TO 'ユーザー名@ホスト名';
```

そして、今回のレッスンで使われていたもの。

```mysql
grant all on dotinstall_poll_php.* to dbuser@localhost identified by '********';
```

これはdotinstall_poll_phpテーブルに対する、すべての権限（SELECT,UPDATEなど）dbuser@localhostに与えています。そして、このユーザーのパスワードを'********'に設定しています。

権限は、ONの次で４つの有効範囲をできます。

```mysql
// グローバルレベル:
GRANT 権限 ON *.* TO user;

// データベースレベル:
GRANT 権限 ON db_name.* TO user;

// テーブルレベル:
GRANT 権限 ON db_name.table_name TO user;

// カラムレベル:
GRANT 権限 (カラム1, カラム2, ...) ON db_name.table_name TO user;
```

こちらによいサンプルが紹介されています。

[Mysqlのユーザ/権限管理](http://d.hatena.ne.jp/yutakikuchi/20110609/1307581865)

また、この記事でもGRANT文は取り上げました。

[【ドットインストール】ページング機能の解説](http://program-blog.net/2013/10/%E3%83%89%E3%83%83%E3%83%88%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%AE%E3%83%9A%E3%83%BC%E3%82%B8%E3%83%B3%E3%82%B0%E6%A9%9F%E8%83%BD%E3%81%AE%E8%A7%A3%E8%AA%AC.html#GRANT_02)

**USE文**

「USE データベース名」でデータベースに接続します。一度接続すれば、FROMでテーブルだけ指定すればよくなります。データベース名を省略できるようになりますが、省略しなくてもよいです。

**ユニークキー**

ユニークキーは値が重複することはできません。プライマリキーとは違いNULLを格納できます。また、NULLの値だけ重複することができます。

ここで、今回のデータベースの全体図を把握しておきましょう。

カラム           | 内容              | 補足          
------------- | --------------- | ------------
id            | 自動連番            |             
answer        | 回答              | 将来的な拡張のため文字列
remote_addr   | 回答者のIPアドレス      |             
user_agent    | ブラウザの情報         |             
answer_date   | 回答日             |             
modified      | 更新日             |             
unique_answer | IP * ブラウザ * 回答日 | 1日に1回という制限  

### セッションの有効範囲を理解する #03

[\#03 設定ファイルを作ろう](http://dotinstall.com/lessons/poll_php_v2/9703)

PHPのデータベース操作についてですが、こちらで専用の記事を作っています。図を用意していますので、ご覧ください。

[PDOを使ったPHPでのデータベース基本操作](http://program-blog.net/2013/10/pdo%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Fphp%E3%81%A7%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C.html)

この章では1つの関数だけ確認しておきましょう。

[session_set_cookie_params(有効期限(秒数), 動作するパス)](http://php.net/manual/ja/function.session-set-cookie-params.php)

この関数の引数は最大5つです。詳しくはリンク先のPHPマニュアルを御覧ください。

```php
session_set_cookie_params(0, '/poll_php/');
```

今回のレッスンでは有効期限は0、つまりブラウザが閉じられるまでで、有効範囲はpoll_phpというディレクトリ以下となっています。また、パスではなくドメインで有効範囲を指定する場合は3つ目の引数で指定します。

この関数の使用注意としては[session_start()](http://www.php.net/manual/ja/function.session-start.php)を呼び出す前に[session_set_cookie_params](http://php.net/manual/ja/function.session-set-cookie-params.php)を呼び出しておく必要があります。

余談ですが、PHPマニュアルを見ると第1引数の有効期限が英語だとlifetimeという表現になっています。「命の時間」ですか。「生きている時間」、つまり「有効期限」となっているのですね。英語はこういうところが面白い表現だなと思います。

### 関数と設定のファイル分け #04

[\#04 よく使う関数を登録しておこう](http://dotinstall.com/lessons/poll_php_v2/9704)

PHPのデータベース操作についてですが、こちらで専用の記事を作っています。図を用意していますので、ご覧ください。

[PDOを使ったPHPでのデータベース基本操作](http://program-blog.net/2013/10/pdo%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Fphp%E3%81%A7%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C.html)

関数定義についてですが、h()はPHPではおなじみのhtmlspecialchars()の省略形です。そして、もう1つのconnectDb()。これはPDOでデータベース接続してPDOオブジェクトを返します。

私はdb.phpというファイルにデータベースの接続を行うまでの処理をまとめていましたが、データベース接続だけ関数定義にして、定数は定数でファイルを分けた方がいいのかなと思いました。

```php
<?php
/*** db.php ***/

define('DB_HOST', 'localhost');
define('DB_NAME', 'データベース名');
define('DB_USER', 'ユーザー名');
define('DB_PASSWORD', 'パスワード');
// 文字化け対策
$options = array(PDO::MYSQL_ATTR_INIT_COMMAND=-->"SET CHARACTER SET 'utf8'");

error_reporting(E_ALL & ~E_NOTICE);

// データベースの接続
try {
    $dbh = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME, DB_USER, DB_PASSWORD, $options);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    echo $e->getMessage();
    exit;
}
?>
```

### idで選択されものを判別 #05
![\#05 投票画面を作ってみよう](https://qiita-image-store.s3.amazonaws.com/0/62881/2d2d9ce4-e4d1-7b3a-a094-01dc35f0ad5e.gif "voting_system001.gif")


画面を図解。

[![図：投票システムのデザイン](http://program-blog.net/wp-content/uploads/2013/10/voting_system001.gif)](http://program-blog.net/wp-content/uploads/2013/10/voting_system001.gif)

### 見栄えの変化はCSSで定義しておく #06

[\#06 投票結果を取得してみよう](http://dotinstall.com/lessons/poll_php_v2/9706)

クリックイベントハンドラの中のthisはクリックされたものを指します。今回はクラスcandidateに対してクリックイベントを設定していますので、そのクラスが付けられている画像をthisの対象となります。

今回のクリックイベントの内容は以下の通りです。

1.  candidateクラスの付いてるものからselectedクラスを取り除く。
2.  クリックされたものにselectedクラスをつける。
3.  IDanswer、つまりテキストフィールドの値を画像のid値に書き換える。

なぜ、1についての処理ですが、何故candidateクラス**全体**にremoveClass()を行うのかと言うと、  
画像がクリックする度に前にクリックされた画像にremoveClass()を行おうとすると、クリックされた履歴を管理しないといけないですし、すべての画像のクラスを取り除いたほうが安全だからです。保証ですね。

### CSRFで他のサイトからのデータ送信を防ぐ #07

[\#07 CSRF対策を施そう](http://dotinstall.com/lessons/poll_php_v2/9707)

CSRFって何？という方がいると思いますので、以前の解説記事を貼っておきます。

[【ドットインストール】PHPで作る「簡易掲示版」の解説](http://program-blog.net/2013/10/%E3%80%90%E3%83%89%E3%83%83%E3%83%88%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%80%91php%E3%81%A7%E4%BD%9C%E3%82%8B%E3%80%8C%E7%B0%A1%E6%98%93%E6%8E%B2%E7%A4%BA%E7%89%88%E3%80%8D.html#CSRF_08-09)

今回はちゃんと自分の指定したformからの送信を受け付けるように設定しています。そのためにトークンを使っています。実際のサービスだと、ネット銀行やオンラインゲームとかのワンタイムパスワードみたいなものですね。

他の例をあげれば、サイトにログインするときに「画像に出ている英数字を入力してください」と出ますが、あれもCSRF対策の一種です。その場でランダムな値を発生させてそれをパスワードとしています。このパスワードがちゃんとこのサイトからデータを受診したという証明になります。CSRF対策がないと他のサイトからデータを送信され、それが悪意のあるプログラムなんかだったりしたら涙目です。例えば、リンクにプログラムを忍ばせておくとかですね。


![図：CSRFの例](https://qiita-image-store.s3.amazonaws.com/0/62881/29b1f35a-4bab-856e-a564-726dbded6670.gif "simple_bulletin_board003.gif")


### in_arrayで回答と正解かを判定する #08

[\#08 エラーチェックをしてみよう](http://dotinstall.com/lessons/poll_php_v2/9708)

**[in_array(探す値, 検索する配列)](http://php.net/manual/ja/function.in-array.php)**

```php
if (!in_array($_POST['answer'], array(1, 2, 3, 4))) {}
```

このコードは1~4にanswerが含まれているかを判定します。そして、その否定ですから、1~4にanswerが含まれていない、つまり答えが1~4ではない時にこのif文は実行されます。

### excute()の戻り値で投票制限を判断 #09-10

[\#09 投票結果を格納しよう (1)](http://dotinstall.com/lessons/poll_php_v2/9709)

[\#10 投票結果を格納しよう (2)](http://dotinstall.com/lessons/poll_php_v2/9710)

insert文で「:~」が使われていますが、これはパラメータマーカを指定しています。変数みたいなもんです。:（コロン）を使って、先に宣言だけしておいて、後から代入できるという便利なものです。

SQL文を実行する時にパラメータマーカを指定する場合は、query()ではなくprepareを使います。そして、[bindParam()](http://php.net/manual/ja/pdostatement.bindparam.php)パラメータマーカに値をセットしてexcute()でSQLを実行します。excute()の引数でパラメータマーカを指定もできます。

excute()はパラメータマーカが異常であると、失敗したとしてfalseを返します。成功した場合はtrueを返します。なので、これを利用して1日1回の投稿制限をかけています。パラメータマーカにユニークキーが含まれているので、値が重複できません。（この場合の値とはユーザーを識別する情報）

```php
if ($stmt->execute($params)) {
    $msg = "投票ありがとうございました！";
} else {
    $err = "投票は1日1回までです！";
}
```