1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WEBページのコメント機能の実装

Last updated at Posted at 2024-12-12

はじめに

自作のブログなどでコメント機能を実装したいときに、役立つかもしれません

クライアント側の実装

htmlの要素であるformタグを使って、サーバーにデータ送信します
formタグの属性のaction属性に送信先、method属性にPOSTかGETを記述します。
今回は送信先を、comment-send.php,メソッドはPOSTとします。

main.php
<!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タグのtypesubmitにすることでボタンを押すことでサーバーに送信できます。
(この状態で押しても意味はないですが)
CSSは適宜デザインしてください。

サーバー側の実装

データの取得

クライアント側から送信されたデータはphpのスーパーグローバル変数$_POSTに格納されています
この変数は連想配列になっていて、keyはname属性の値です。
$_POST['text']で入力したテキストを取得できます。

comment-send.php
$text = $_POST['text'];

エラーの回避

comment-send.phpもサーバーに設置するので、設置した状態でこのファイルに直接アクセスするとエラーが起きます。fatal errorではないので、処理は継続されますが、プログラムとしてはよくないので修正します。
エラーの原因は、$_POST['text']が存在しないことです。POSTしてないからそうですよね。
これを解決するのがisset()関数です。引数に変数の名前を入れ、存在していたらtrueを返します。
falseになったときのメッセージも設定しておきましょう。

comment-send.php
//エラーメッセージ設定(ヒアドキュメント)
$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クラスを使用します。
数回データベースに接続するプログラムを書くので、データベースのパスワードなどを使い回すために、使う情報を別のファイルに記述すると便利です。

key.php
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';

そして変数に定数を代入します。

comment-send.php
//読み込み
require '../key.php';

$dbPass = DB_PASSWORD;
$dbUser = DB_USERNAME;
$dbHost = DB_HOST;
$dbName = DB_NAME;

データベースに接続

phpの機能、PDOtry-catch(try中にエラーをキャッチしたらcatch中のの処理を行う)を用いて接続します。

comment-send.php
$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インジェクション対策ができます。

comment-send.php
//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直下にあるとします。

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();
}
?>

SQL文のORDER BY textNumber DESCでtextNumberの降順で抽出し、limit 50で最大50行抽出としています。
fetchAllは抽出されたすべてのデータを返すメソッドです。(fetchは1行のみ)

JS側

コメントデータを取得→データに基づいてHTML要素を作成→表示させる
という処理にしています。

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;

これでコメントの投稿と表示ができるようになりました。

.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でもいけるみたいです。

comment-send.php
$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にはtruefalseが入っています。

終わり

だいぶ内容を詰め込みました。書いていて楽しかったです。サンプルコードをすべて載せておきます。

main.php
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
main.css
textarea {
    resize: none;
    width:300px;
    height:200px;
    font-size: 20px;
}
main.js
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
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
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
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','シークレットキー');
?>
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?