概要
本記事では、PHPを用いた投票システムの実装に焦点を当て、その技術的な側面を詳細に解説します。投票システムは、Webアプリケーションにおいてユーザーの意見を集約し、意思決定を支援するための基本的な機能です。その実装には、データの永続化、セキュリティ、UI/UXの考慮など、多岐にわたる技術要素が関わってきます。ここでは、シンプルながらも堅牢な投票システムをPHPで構築するための、データベース設計、バックエンドロジック、そしてフロントエンドとの連携について掘り下げていきます。特に、SQLインジェクション対策や二重投票防止といった、実運用で不可欠なセキュリティ対策についても具体的に解説し、読者が実践的な知識を習得できるよう努めます。
詳細解説
データベース設計
投票システムの中核となるのは、投票内容と投票結果を格納するデータベースです。ここでは、最小限のテーブル構成で効率的にデータを管理する方法を提案します。
テーブル構成
1. **`polls` テーブル**: 投票の質問内容や設定を格納します。
* `id` (INT, PRIMARY KEY, AUTO_INCREMENT): 投票の一意なID。
* `question` (VARCHAR(255), NOT NULL): 投票の質問文。
* `created_at` (TIMESTAMP, DEFAULT CURRENT_TIMESTAMP): 投票が作成された日時。
2. **`options` テーブル**: 各投票の選択肢を格納します。
* `id` (INT, PRIMARY KEY, AUTO_INCREMENT): 選択肢の一意なID。
* `poll_id` (INT, NOT NULL): 関連する投票の`id` (FOREIGN KEY)。
* `option_text` (VARCHAR(255), NOT NULL): 選択肢のテキスト。
* `vote_count` (INT, DEFAULT 0): その選択肢への投票数。
3. **`votes` テーブル**: 誰がどの投票のどの選択肢に投票したかの記録を格納します。これは二重投票防止のために不可欠です。
* `id` (INT, PRIMARY KEY, AUTO_INCREMENT): 投票記録の一意なID。
* `poll_id` (INT, NOT NULL): 投票の`id` (FOREIGN KEY)。
* `option_id` (INT, NOT NULL): 選択された選択肢の`id` (FOREIGN KEY)。
* `user_identifier` (VARCHAR(255), NOT NULL): 投票者を識別するための情報(IPアドレス、セッションID、ログインユーザーIDなど)。
* `voted_at` (TIMESTAMP, DEFAULT CURRENT_TIMESTAMP): 投票日時。
リレーションシップ
* `polls` テーブルと `options` テーブルは1対多の関係です。一つの投票には複数の選択肢が存在します。
* `options` テーブルと `votes` テーブルは1対多の関係です。一つの選択肢には複数の投票記録が存在します。
* `polls` テーブルと `votes` テーブルは1対多の関係です。一つの投票には複数の投票記録が存在します。
バックエンドロジック (PHP)**
PHPでは、データベース操作、投票処理、結果表示などのロジックを実装します。ここでは、PDO (PHP Data Objects) を使用してデータベースに安全に接続し、操作する方法を解説します。
データベース接続
PDOは、様々なデータベースに対応した統一的なインターフェースを提供し、プレースホルダの使用によりSQLインジェクションを防ぐことができます。
setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // エラーモードを例外に設定
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // プリペアドステートメントのエミュレーションを無効化
} catch (PDOException $e) {
die(‘データベース接続エラー: ‘ . $e->getMessage());
}
?>
投票の表示 (投票画面)**
ユーザーが投票するための質問と選択肢を表示する処理です。
prepare(“SELECT id, question FROM polls WHERE id = :poll_id”);
$stmt->bindParam(‘:poll_id’, $pollId, PDO::PARAM_INT);
$stmt->execute();
$poll = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$poll) {
die(“指定された投票が見つかりません。”);
}
// 投票の選択肢を取得
$stmt = $pdo->prepare(“SELECT id, option_text FROM options WHERE poll_id = :poll_id”);
$stmt->bindParam(‘:poll_id’, $pollId, PDO::PARAM_INT);
$stmt->execute();
$options = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($options)) {
die(“この投票には選択肢が設定されていません。”);
}
} catch (PDOException $e) {
die(‘投票情報の取得中にエラーが発生しました: ‘ . $e->getMessage());
}
?>
* `htmlspecialchars()` 関数は、クロスサイトスクリプティング (XSS) 攻撃を防ぐために、HTMLエンティティに変換します。
投票処理 (vote.php)**
ユーザーからの投票を受け付け、データベースに記録する処理です。二重投票防止策を講じます。
prepare(“SELECT COUNT(*) FROM votes WHERE poll_id = :poll_id AND user_identifier = :user_identifier”);
$stmt->bindParam(‘:poll_id’, $pollId, PDO::PARAM_INT);
$stmt->bindParam(‘:user_identifier’, $userIdentifier, PDO::PARAM_STR);
$stmt->execute();
$voteCount = $stmt->fetchColumn();
if ($voteCount > 0) {
die(“この投票には既に投票済みです。”);
}
// 投票のコミット (トランザクションを使用)
$pdo->beginTransaction();
// votesテーブルに投票記録を追加
$stmt = $pdo->prepare(“INSERT INTO votes (poll_id, option_id, user_identifier) VALUES (:poll_id, :option_id, :user_identifier)”);
$stmt->bindParam(‘:poll_id’, $pollId, PDO::PARAM_INT);
$stmt->bindParam(‘:option_id’, $optionId, PDO::PARAM_INT);
$stmt->bindParam(‘:user_identifier’, $userIdentifier, PDO::PARAM_STR);
$stmt->execute();
// optionsテーブルのvote_countをインクリメント
$stmt = $pdo->prepare(“UPDATE options SET vote_count = vote_count + 1 WHERE id = :option_id AND poll_id = :poll_id”);
$stmt->bindParam(‘:option_id’, $optionId, PDO::PARAM_INT);
$stmt->bindParam(‘:poll_id’, $pollId, PDO::PARAM_INT); // 念のためpoll_idでもチェック
$stmt->execute();
$pdo->commit(); // トランザクションをコミット
// 投票完了後、結果表示ページへリダイレクト
header(“Location: results.php?poll_id=” . $pollId);
exit;
} catch (PDOException $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack(); // エラー発生時はロールバック
}
die(‘投票処理中にエラーが発生しました: ‘ . $e->getMessage());
}
?>
* `filter_input()` 関数は、外部からの入力をフィルタリングし、不正な値を排除します。
* `$_SERVER[‘REMOTE_ADDR’]` は、クライアントのIPアドレスを取得します。ただし、プロキシ環境下では正確なIPアドレスが得られない場合があるため、より高度な識別子が必要になることもあります。
* トランザクション (`beginTransaction()`, `commit()`, `rollBack()`) を使用することで、投票記録の追加と投票数の更新がアトミックに行われ、データの一貫性が保たれます。
投票結果の表示 (results.php)**
投票結果をグラフなどで分かりやすく表示する処理です。
prepare(”
SELECT
o.option_text,
o.vote_count,
p.question
FROM options o
JOIN polls p ON o.poll_id = p.id
WHERE o.poll_id = :poll_id
ORDER BY o.id
“);
$stmt->bindParam(‘:poll_id’, $pollId, PDO::PARAM_INT);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($results)) {
die(“指定された投票の結果が見つかりません。”);
}
$pollQuestion = $results[0][‘question’]; // 質問は最初の行から取得
} catch (PDOException $e) {
die(‘投票結果の取得中にエラーが発生しました: ‘ . $e->getMessage());
}
?>
結果
