はじめに
自作のブログなどでコメント機能を実装したいときに、役立つかもしれません
クライアント側の実装
htmlの要素であるform
タグを使って、サーバーにデータ送信します
form
タグの属性のaction
属性に送信先、method
属性にPOSTかGETを記述します。
今回は送信先を、comment-send.php
,メソッドはPOSTとします。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Comment</title>
</head>
<body>
<form action="comment-send.php" method ="post">
<input type="text" name="name" placeholder="お名前"><br>
<textarea name="text" placeholder="コメントの内容"></textarea>
<input type="submit" value="コメントを送信する" id="send">
</form>
</body>
name
属性をform
タグの中の要素に持たせることで、送信先でデータを受け取ることができます。
input
タグのtype
をsubmit
にすることでボタンを押すことでサーバーに送信できます。
(この状態で押しても意味はないですが)
CSSは適宜デザインしてください。
サーバー側の実装
データの取得
クライアント側から送信されたデータはphpのスーパーグローバル変数$_POST
に格納されています
この変数は連想配列になっていて、keyはname
属性の値です。
$_POST['text']
で入力したテキストを取得できます。
$text = $_POST['text'];
エラーの回避
comment-send.php
もサーバーに設置するので、設置した状態でこのファイルに直接アクセスするとエラーが起きます。fatal errorではないので、処理は継続されますが、プログラムとしてはよくないので修正します。
エラーの原因は、$_POST['text']
が存在しないことです。POSTしてないからそうですよね。
これを解決するのがisset()
関数です。引数に変数の名前を入れ、存在していたらtrue
を返します。
false
になったときのメッセージも設定しておきましょう。
//エラーメッセージ設定(ヒアドキュメント)
$errorMesseage = <<< EOD
<p>無効なページです 3秒後に戻ります</p>
<script>
setTimeout(function(){
window.location.href = "main";
},3000)
</script>
EOD;
//ポストの受取
$text;
$name;
if(isset($_POST['text'] && isset($_POST['name'])){
$text = $_POST['text'];
$name = $_POST['name'];
}
else{
echo $errorMesseage;
exit;
}
データの登録
データを取得しただけでは意味がありません。データを何かしらの形で保存しましょう。
今回はMySQLを使います。
まずは、次のSQL文を実行し、テーブルを作成します。テーブル名はcomment
としています。
CREATE TABLE `データベース名`.`comment` (`name` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL , `text` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL ,`textNumber` INT NOT NULL , UNIQUE (`textNumber`)) ENGINE = InnoDB CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;
name | text | textNumber |
---|
というのが作成されるはずです。(カラムは適宜追加してください)
textNumber
は一番最初のコメントを0としてデータ毎に1,2と増えるようにします。(ユニークキーとしているので、値は被らないように)
簡略化
データを取得した状態でデータベースに接続します。今回はPDO
クラスを使用します。
数回データベースに接続するプログラムを書くので、データベースのパスワードなどを使い回すために、使う情報を別のファイルに記述すると便利です。
define('DB_PASSWORD','');//MySQLのパスワード
define('DB_USERNAME','');//MySQLのユーザー名
define('DB_HOST','');//ホスト名(Xserverの場合はlocalhost)
define('DB_NAME','');//データベース名
このファイルは外部からアクセスできないようにドキュメントルート外に設置しましょう。(Xserverではpublic_html
がドキュメントルート)
今回はディレクトリ構造を以下とします。
┌public_html─┬─comment-send.php
│ └─main.php
└key.php
このファイルをcomment-send.php
で読み込みましょう。
読み込むためにはrequire 'ファイル名'
を使います。ファイル名は相対パスも絶対パスでもいいです。
require '../key.php';
そして変数に定数を代入します。
//読み込み
require '../key.php';
$dbPass = DB_PASSWORD;
$dbUser = DB_USERNAME;
$dbHost = DB_HOST;
$dbName = DB_NAME;
データベースに接続
phpの機能、PDO
とtry-catch
(try中にエラーをキャッチしたらcatch中のの処理を行う)を用いて接続します。
$dsn = "mysql:host={$dbHost};dbname={$dbName}";
try{
$dbh = new PDO($dsn,$dbUser,$dbPass);//接続
echo "接続に成功";
$dbh = null;//切断
} catch(PDOException $e){
exit('接続に失敗: ' . $e->getMessage());//エラーメッセージ デバッグ以外はコメントアウト
}
INSERT文
MySQLにデータを追加するときはINSERTを使います。
INSERT INTO comment(name, text, textNumber) VALUES ('名前','テキスト','テキスト番号');
SELECT文
textNumberの値は被らないようにしたいので、最大値を取得します。
SELECT MAX(textNumber) FROM comment;
SQL文の実行
SQLはqueryメソッドで実行できます。
$sql = "SQL文";
$dbh->query($sql);
このqueryメソッドは1行しか実行できません。
SQLインジェクション対策とXSS対策
データベースにいれる値は動的である必要があります。
ただしデータにいれる値の中に悪意のあるテキストが含まれているかもしれません。
htmlspecialchars
関数でサニタイズできます。
htmlspecialchars("なにかしらのテキスト", ENT_QUOTES, "UTF-8");
prepare
メソッド,execute
メソッド,bindValue
メソッドを使うことでSQLインジェクション対策ができます。
//textnumberの最大値の取得
$sql = "SELECT MAX(textNumber) FROM comment";
//sqlの実行
$maxSth = $dbn->query($sql);
$maxResult = $maxSth->fetch(PDO::FETCH_ASSOC);//$maxResult['MAX(textNumber)']に最大値が入っている
if($maxResult['MAX(textNumber)'] === null) $maxResult['MAX(textNumber)'] = 0;//最初のコメント
else $maxResult['MAX(textNumber)']++;//最初以外は+1
//データの登録
//サニタイズ
$safeText = htmlspecialchars($text, ENT_QUOTES, "UTF-8");
$safeName = htmlspecialchars($name, ENT_QUOTES, "UTF-8");
$sql = "INSERT INTO comment(name, text, textNumber) VALUES (:name,:text,:textNumber)";
//sqlの準備
$subscSth = $dbh->prepare($sql);
//値のバインド
$subscSth->bindValue(':name', $safeName);
$subscSth->bindValue(':text', $safeText);
$subscSth->bindValue(':textNumber', $maxResult['MAX(textNumber)']);
//sqlの実行
$subscSth->execute();
これでSQL上からは送られたコメントを見ることができるようになりました。
$maxResult['MAX(textNumber)'] == null
としてしまうと、0がnull扱いとなり常にこの文が実行されます。
コメントを誰でも見られるようにする
SELECT文で取得してもいいし、自分でAPIを作ってもいい気がします。
今回は素直にSELECT文で取得して表示させます。
JSでGETして、返された値を使って表示させることにします。
JSでGET
JS側
JSでGETするにはfetch
を使います。リクエスト先はgetcomment.php
とします。
function GetComment(){
fetch('getcomment.php')
.then(response => response.json())
.then(data =>{
//dataを使って処理
})
}
dataはjson形式ではなく、オブジェクトなので連想配列みたいにアクセスできます。
json()がjsonを読み取って変換してくれます。(とてもgood)
PHP側
パラメータなどは受け取らないので$_GET
とかは必要ありません
JSに返すときは、json形式にしてecho
などの出力をすると返すことが来ます
$data['test'] = "テスト"
echo json_encode($data);//{“test” : “テスト”}が返される。
echo
のあとすぐに、exit
をすると返されないことがある。
実装
PHP側
getcomment.php
はpublic_html直下にあるとします。
<?php
require '../key.php';
$dbPass = DB_PASSWORD;
$dbUser = DB_USERNAME;
$dbHost = DB_HOST;
$dbName = DB_NAME;
$dsn = "mysql:host={$dbHost};dbname={$dbName}";
try{
$dbh = new PDO($dsn,$dbUser,$dbPass);//接続
$sql = "SELECT * FROM comment ORDER BY textNumber DESC limit 50";
$dataSth = $dbh->query($sql);
$dataResult = $dataSth->fetchAll(PDO::FETCH_ASSOC);
$dbh = null;//切断
echo json_encode($dataResult);//出力
} catch(PDOException $e){
exit();
}
?>
SQL文のORDER BY textNumber DESC
でtextNumberの降順で抽出し、limit 50
で最大50行抽出としています。
fetchAll
は抽出されたすべてのデータを返すメソッドです。(fetch
は1行のみ)
JS側
コメントデータを取得→データに基づいてHTML要素を作成→表示させる
という処理にしています。
function GetComment(){
const form = document.getElementsByTagName('form');
fetch('getcomment.php')
.then(response => response.json())
.then(CommentDate =>{
CommentDate.forEach(function(data){
//form要素の下に挿入
form[0].insertAdjacentElement("afterend", CreateComment(data));
})
})
}
function CreateComment(Data){
//要素を作成
const newDiv = document.createElement("div");
const newCommentArea = document.createElement('textarea');
const username = document.createElement('label');
//取得したデータを代入
username.innerText = Data['name'];
newCommentArea.value = Data['text'];
newCommentArea.readOnly = true;//テキストエリアを読み込み専用に
//divの子要素として追加
newDiv.appendChild(username);
newDiv.appendChild(newCommentArea);
//クラスを付与
newDiv.setAttribute("class","comments");
return newDiv;
}
//すべてのDOMを読み込んだときに起動
window.onload = GetComment;
これでコメントの投稿と表示ができるようになりました。
.then
で渡したあとのデータは別の関数などに渡さずに、内部で処理
reCAPTCHAの実装
ついでに、スパム対策としてreCAPTCHAを実装します。
最新であるreCAPTCHA v3は、チェックボックスはなく、スコア(0.0~1.0)に基づいて、判定する仕組みになっています。
今回は馴染みあるチェックボックスで判定するv2を採用します。
reCAPTCHAに登録
以下のサイトにアクセスし、サイトを登録します。
reCAPTCHAタイプ
はv2選択してください。
https://www.google.com/recaptcha/admin/create
そうするとサイトキーとシークレットキーが発行されます。
シークレットキーは公開しないようにしてください。
この2つのキーもファイルから読み込むようにします。
以下をkey.php
に追加します。
define('V2_SITEKEY','サイトキー');
define('V2_SECRETKEY','シークレットキー');
導入方法
クライアント側
form
タグの中に
<div class="g-recaptcha" data-sitekey="サイトキー" data-callback="Callback" data-expired-callback="expiredCallback"></div>
を追加(サイトキーは直接書いてもいいし、ファイルを読み込んでecho
で出力するでもいいです)
body
の最後に
<script>
var Callback = function(res){
//reCAPTCHA承認時の処理
}
var expiredCallback = function(){
//承認失敗時の処理
}
</script>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
を追加
submit
時にレスポンストークンも一緒にPOSTされます。
サーバー側
レスポンストークンは$_POST['g-recaptcha-response']
に格納されています。このトークンが有効であるかどうかをAPIを使って判定します。
PHPでAPIを叩く方法(サーバーにリクエストを送信する)は複数ありますが、今回はcurl
関数を使います。
メソッドはPOSTです。GETでもいけるみたいです。
$secretKey = V2_SECRETKEY;
$url = 'https://www.google.com/recaptcha/api/siteverify';//APIのURL
$params = "secret={$secretKey}&response={$recaptcha}";//送信する情報
$header = ['Content-Type: application/x-www-form-urlencoded'];//ヘッダー情報
$ch = curl_init();//初期化
//転送設定
curl_setopt($ch, CURLOPT_POST, true);//POST設定をtrueに
curl_setopt($ch, CURLOPT_URL, $url);//リクエスト先指定
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);//リクエストボディ
curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);//返り値を文字列に
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);//ヘッダー指定
//実行して格納
$result = json_decode(curl_exec($ch));
//閉じる
curl_close($ch);
if(!$result->success) exit;//承認失敗時
reCAPTCHAの返り値はJSON形式なので、json_decode
で変換しています。json_decode
の第二引数をtrue
に指定すると、連想配列として変換することもできます。
$result->success
にはtrue
かfalse
が入っています。
終わり
だいぶ内容を詰め込みました。書いていて楽しかったです。サンプルコードをすべて載せておきます。
main.php
<?php
//キーファイルの読み込み
require '../key.php';
$sitekey = V2_SITEKEY;
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Comment</title>
<link rel="stylesheet" href="main.css?<?php echo date('YmdHis', filemtime('main.css')); ?>">
</head>
<body>
<form action="comment-send" id="form" method ="post">
<input type="text" name="name" placeholder="お名前"><br>
<textarea id="text" name="text" placeholder="コメントの内容"></textarea>
<div class="g-recaptcha" id="recaptcha" data-sitekey="<?php echo $sitekey;?>" data-callback="Callback" data-expired-callback="expiredCallback"></div>
<input type="submit" value="コメントを送信する" id="send" disabled>
</form>
<script>
var Callback = function(res) { //コールバック関数の定義
document.getElementById("send").disabled = false;
};
var expiredCallback = function() { //コールバック関数の定義
document.getElementById("send").disabled = true;
};
</script>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<script type="text/javascript" src="main.js?<?php echo date('YmdHis', filemtime('main.js')); ?>"></script>
</body>
main.css
textarea {
resize: none;
width:300px;
height:200px;
font-size: 20px;
}
main.js
function GetComment(){
const form = document.getElementsByTagName('form');
fetch('getcomment.php')
.then(response => response.json())
.then(CommentDate =>{
CommentDate.forEach(function(data){
//form要素の下に挿入
form[0].insertAdjacentElement("afterend", CreateComment(data));
})
})
}
function CreateComment(Data){
//要素を作成
const newDiv = document.createElement("div");
const newCommentArea = document.createElement('textarea');
const username = document.createElement('label');
//取得したデータを代入
username.innerText = Data['name'];
newCommentArea.value = Data['text'];
newCommentArea.readOnly = true;
//divの子要素として追加
newDiv.appendChild(username);
newDiv.appendChild(newCommentArea);
newDiv.setAttribute("class","comments");
return newDiv;
}
//すべてのDOMを読み込んだときに起動
window.onload = GetComment;
getcomment.php
<?php
require '../key.php';
$dbPass = DB_PASSWORD;
$dbUser = DB_USERNAME;
$dbHost = DB_HOST;
$dbName = DB_NAME;
$dsn = "mysql:host={$dbHost};dbname={$dbName}";
try{
$dbh = new PDO($dsn,$dbUser,$dbPass);//接続
$sql = "SELECT * FROM comment ORDER BY textNumber DESC limit 50";
$dataSth = $dbh->query($sql);//クエリを実行
$dataResult = $dataSth->fetchAll(PDO::FETCH_ASSOC);
$dbh = null;//切断
echo json_encode($dataResult);//出力
} catch(PDOException $e){
exit();
}
?>
comment-send.php
<?php
//キーファイル読み込み
require '../../key.php';
$secretKey = V2_SECRETKEY;
$dbPass = DB_PASSWORD;
$dbUser = DB_USERNAME;
$dbHost = DB_HOST;
$dbName = DB_NAME;
//エラーメッセージ設定
$errorMesseage = <<< EOD
<p>無効なページです 3秒後に戻ります</p>
<script>
setTimeout(function(){
window.location.href = "main";
},3000)
</script>
EOD;
//成功メッセージ設定
$ssuccessrMesseage = <<< EOD
<p>コメントに成功しました 3秒後に戻ります</p>
<script>
setTimeout(function(){
window.location.href = "main";
},3000)
</script>
EOD;
//ポストの受取
$text;
$recaptcha;
$name;
if(isset($_POST['text']) && isset($_POST['g-recaptcha-response']) && isset($_POST['name'])){
$text = $_POST['text'];
$recaptcha = $_POST['g-recaptcha-response'];
$name = $_POST['name'];
}
else{
echo $errorMesseage;
exit;
}
//reCAPTCHA API起動
$url = 'https://www.google.com/recaptcha/api/siteverify';//APIのURL
$params = "secret={$secretKey}&response={$recaptcha}";//送信する情報
$header = ['Content-Type: application/x-www-form-urlencoded'];//ヘッダー情報
$ch = curl_init();//初期化
//転送設定
curl_setopt($ch, CURLOPT_POST, true);//POST設定をtrueに
curl_setopt($ch, CURLOPT_URL, $url);//リクエスト先指定
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);//リクエストボディ
curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);//返り値を文字列に
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);//ヘッダー指定
//実行して格納
$result = json_decode(curl_exec($ch));
//閉じる
curl_close($ch);
if(!$result->success){
echo $errorMesseage;//トークンが不正なとき
exit;
};
$dsn = "mysql:host={$dbHost};dbname={$dbName}";
try{
$dbh = new PDO($dsn,$dbUser,$dbPass);//接続
$sql = "SELECT MAX(textNumber) FROM comment";
//sqlの実行
$maxSth = $dbh->query($sql);
$maxResult = $maxSth->fetch(PDO::FETCH_ASSOC);//$maxResult['MAX(textNumber)']に最大値が入っている
if($maxResult['MAX(textNumber)'] === null) $maxResult['MAX(textNumber)'] = 0;//最初のコメント
else $maxResult['MAX(textNumber)']++;//最初以外は+1
//データの登録
//サニタイズ
$safeText = htmlspecialchars($text, ENT_QUOTES, "UTF-8");
$safeName = htmlspecialchars($name, ENT_QUOTES, "UTF-8");
$sql = "INSERT INTO comment(name, text, textNumber) VALUES (:name,:text,:textNumber)";
//sqlの準備
$subscSth = $dbh->prepare($sql);
//値のバインド
$subscSth->bindValue(':name', $safeName);
$subscSth->bindValue(':text', $safeText);
$subscSth->bindValue(':textNumber', $maxResult['MAX(textNumber)']);
//sqlの実行
$subscSth->execute();
echo $ssuccessrMesseage;//コメントできたとき
$dbh = null;//切断
} catch(PDOException $e){
exit();
}
?>
key.php
<?php
//データベース用
define('DB_PASSWORD','');//MySQLのパスワード
define('DB_USERNAME','');//MySQLのユーザー名
define('DB_HOST','');//ホスト名(Xserverの場合はlocalhost)
define('DB_NAME','');//データベース名
//reCAPTCHA用
define('V2_SITEKEY','サイトキー');
define('V2_SECRETKEY','シークレットキー');
?>