【自作】PHPでチャット機能の作り方!LINEのような会話形式のプログラムのコードを徹底解説!
この記事からわかること
- phpで作るチャット機能の作り方
- チャット機能の仕組み
- 一緒に作りながら学べるチャット機能の説明
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
実際に私が作ったチャット機能と同じものを真似して作れるように説明していきます。途中に出てくるコードの意味や挙動、仕組みなどPHPを学習しながら解説していきたいと思います!
ちなみに今回作成したのはこのような感じのチャット機能です。
自作したチャット機能(←クリック)自作したチャットの構造と機能
今回はデモページの作成のため1人でも楽しめ、誰でも発言可能なチャット機能にしています。とはいえチャット機能の仕組みは変わらないため実際のDMやLINEのように簡単に流用することもできます。
チャット機能
- 入力欄と送信ボタン
- 送信すると予め決められた画像を表示
- 送信時間も表示
- チャットログはデータベースではなくJSONファイルに格納
- チャットログがあれば常に表示
- チャットログは大きさを指定してCSS:overflow:scrollを指定
- 送信時やリロード時に最新の投稿が見えるようにする
- 画面リロード時のPOSTの二重送信対策
- ユーザーを切り替えることで1人で楽しめるように
チャット構造(必要なファイル)
- chat.php
- chatlog.json
- css/style.css
- css/Font Awesomeファイル(公式サイトよりダウンロード)
- image/チャットアイコン画像2枚
- js/main.js
使用言語と実行環境
- PHP
- HTML
- CSS
- PC:mac(環境)
- Local:MAMP(環境)
HTMLでフォーム部分を作る
まずはHTMLで文書を作成します。ファイル名は「chat.php」とします。のちのちphpファイルとして扱うので拡張子は「.php」でOKです。
HTMLのhead内はいつも通りのCSSやjsファイルの読み込み処理を書いておきます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
<title>チャット</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/fontawesome-free-5.15.3-web/css/all.min.css">
<script src="js/main.js"></script>
</head>
<body>
<main class="main">
<div class="chat-system">
<form class="send-box flex-box" action="chat.php#chat-area" method="post">
<textarea id="textarea" type="text" name="text" rows="1" required placeholder="message.."></textarea>
<input type="submit" name="submit" value="送信" id="search">
<label for="search"><i class="far fa-paper-plane"></i></label>
</form>
</div>
</main>
</body>
</html>
これでHTMLでの基礎部分が完成です。この時点で作成しているのはhead内の文書情報と入力フォーム部分です。
main
の中にchat-system
がありその中にsend-box
でグループ分けしています。chat-system
の中には後で違うグループも入れますが今はsend-box
だけで進めていきます。
入力フォームはチャット入力用のtextareaと送信ボタン用のinputです。
input要素とlabel要素のリンク
input要素
にid属性
を、label要素
にfor属性
を付与することで2つをリンクさせることができます。
↑例えばこのように紙飛行機をクリックしても入力値にフォーカスが当たります。↓コードはこうなります。
<input id="web-chat"><label for="web-chat"> <i class="far fa-paper-plane" style="cursor:pointer;"></i></label>
この状態ではinput要素
も見えてしまっているのでCSSでdisplay:none
を指定してlabel要素だけにすればOKです。
今回はCSSは説明しないのでこの記事の最後にCSSの中身を全て記述しておくのでコピペして使用してください!
form要素でPOSTに値を送信
入力された値はPOSTで受け取りたいためform要素のmethod属性に「POST」を指定します。
飛ばすファイルを指定できるaction属性をchat.php#chat-area
とすることでid:chat-area
の場所に送信後の画面を移動させます。id:chat-area
」はまた後でHTMLに組み込みます。
目次機能の見出しへジャンプするのと同じ仕組みですね!
それではここからはphpでの処理に移っていきます。
POSTで入力値を取得する
では先にやることを整理しておきます。ここでやるのは以下の内容です。
- 入力値を元にデータを連想配列に格納
- 連想配列に格納されたデータをJSONファイルに蓄積
- POSTの二重送信対策のためリダイレクト
POSTで取得した値をchat配列に格納しておきます。POSTを取得する時は最初に$_POST['submit']が存在し、なおかつ中身が「送信」であるかを確認しておきましょう。
<?php
$J_file = "chatlog.json"; // ファイルパス格納
date_default_timezone_set('Asia/Tokyo'); // タイムゾーンを日本にセット
if(isset($_POST['submit']) && $_POST['submit'] === "送信"){ // #1
$chat = [];
$chat["person"] = "person1";
$chat["imgPath"] = "image/person1.png"; //画像ファイル名は任意
$chat["time"] = date("H:i");
$chat["text"] = htmlspecialchars($_POST['text'],ENT_QUOTES);
// 次はここに記述していきます。
} // #1
?>
<!DOCTYPE html>
画像は好きな画像を2枚用意しimageフォルダの中に入れておいてください。
chat配列の中には連想配列で人、画像パス、時間、チャット本文を格納します。
date関数
で「H:i」形式を指定することで24時間表示(例16:20)の時間と分数を取得してくれます。「h:i」(小文字)だと12時間表示(例4:20)になります。
入力値が直接入る「チャット本文」はhtmlspecialchars関数
でエスケープ処理を施しておきます。
関連記事:クロスサイトスクリプティングの対策コード【htmlspecialchars関数】
この4つの項目をフォーマットとしてデータを蓄積していきます。
チャットログをJSONファイルに格納する
今回チャットログの蓄積先はJSONファイルです。JSONファイルはデータの管理、操作が非常に楽なテキストファイルの1種です。データベースを使うより簡単にデータを蓄積、取得することができます。JSONファイルのことをもっと知りたい方は↓こちらの記事をご覧ください!
関連記事:JSONファイルの構造とは?配列と連想配列の違いを理解して正しい記法を覚えよう!
$chat["text"] = htmlspecialchars($_POST['text'],ENT_QUOTES);
// 入力値格納処理
if($file = file_get_contents($J_file)){ // #2
// ファイルがある場合 追記処理
$file = str_replace(array(" ","\n","\r"),"",$file);
$file = mb_substr($file,0,mb_strlen($file)-2);
$json = json_encode($chat);
$json = $file.','.$json.']}';
file_put_contents($J_file,$json,LOCK_EX);
}else{ // #2
// ファイルがない場合 新規作成処理
$json = json_encode($chat);
$json = '{"chatlog":['.$json.']}';
file_put_contents($J_file,$json,FILE_APPEND | LOCK_EX);
} // #2
// 次はここに記述していきます。
} // #1
phpでのJSONファイルの扱い方と挙動はだいぶ長くなるのでこちらの記事にまとめてあります。
関連記事:PHPでJSONファイルの扱い方を徹底解説!作成、追記、読込などの基本的な動作と注意するべきポイントとは?
ここでの処理の流れを確認してみましょう!
phpでjson_encode関数
を使用することでデータを自動でJSON形式にエンコード(符号化)してくれます。エンコード処理の前に連想配列に入れたいのでファイルがある時(追記処理)とない時(新規作成処理)で前準備を工夫しています。
追記処理と言っていますが行っているのはファイルデータを取得してそのデータにチャットログを追加→既存のファイルに上書きです。
// 上のコードを訳すとこのような感じです
// 行番号は同じなので照らし合わせてみてください
// 入力値格納処理
if(ファイルを読み込む){ // #2
// ファイルがある場合
ファイルデータの中の空白や改行を削除
ファイルデータ末尾の']}'の2文字を削除
chat配列のデータをエンコード
連想配列に格納する前準備(後側を付与)ファイルデータ+新チャットログ
ファイルを上書き&データを格納
}else{ // #2
// ファイルがない場合
chat配列のデータをエンコード
連想配列に格納する前準備(前側を付与)
ファイルを作成&データを格納
} // #2
// 次はここに記述していきます。
} // #1
チャットログJSONファイルの構造
JSONファイルのここでの構造はやや複雑になっています。
取り出す際も階層が深い分ややこしくなるので注意してください!
⇩{"連想配列1"}
【1階層】⇩連想配列1→{"chatlog":"[配列1]"}
【2階層】⇩[配列]1→["連想配列2","連想配列2","連想配列2"....]
【3階層】連想配列2(チャットログ)→{"person":"person1","imgPath":"image/person1.png","time":"16:20","text":"チャットの本文"}
3階層目にチャットログを蓄積していきます。
{"chatlog":
[
{"person":"person2","imgPath":"..\/image\/person2.png","time":"22:26","text":"\u30c1\u30e3\u30c3\u30c8\u6a5f\u80fd\u3092\u81ea\u4f5c\u3057\u3066\u3044\u307e\u3059\u3002"},
{"person":"person1","imgPath":"..\/image\/person1.png","time":"22:27","text":"\u30c1\u30e3\u30c3\u30c8\u30ed\u30b0\u306fJSON\u30d5\u30a1\u30a4\u30eb\u3067\u683c\u7d0d\u3057\u3066\u3044\u307e\u3059\u3002"}
]
}
POSTの二重送信対策
POSTで入力値を扱う時に適切な処理をしないと,ページをF5やcommand+Rを押してリロードすると先ほど入力した内容がもう一度送信されてしまいます。
これを防ぐにはPOSTでの操作を終えた後に自分自身にリダイレクトさせるとPOSTの値をリセットすることができます。
詳しくはこちらの記事をご覧ください。
関連記事:【php】リロード対策!postを重複しないように自分自身にリダイレクトさせて解決しよう!
} // #2
// header('Location:https://'.$_SERVER['HTTP_HOST'].dirname($_SERVER['PHP_SELF']).'/chat.php');
header('Location:./chat.php');
exit;
} // #1
// 次はここに記述していきます。
header関数のLocationヘッダにURLを指定することでリダイレクトさせることができます。サーバーやホスト名に依存しないように現在のホスト名やディレクトリを自動で取得できる関数を使って保守性を高めておきます。
※MAMPでこのコードを使うと一度表示されませんが、そのままURLをもう一度読み込めば問題なく動きます。。。
一応保守性の高いコードはコメントアウトしておきローカル環境での動作確認用に相対パスのリダイレクトも載せておいたので切り替えてご使用ください。
チャットログを取得し表示する
続いてJSONファイルに格納してあるログをページ上に表示していきます。ログがあれば表示なければ何も表示しないように分岐できるようにしておきます。
file_get_contents関数
は中身があればそのデータがなければfalseを返す特性を利用して分岐処理させます。
} // #1
if($file = file_get_contents($J_file)){
$file = json_decode($file);
$array = $file->chatlog;
foreach($array as $object){
if(isset($result)){
// 第二回目以降
$result = $result.'<div class="'.$object->person.'"><p class="chat">'.str_replace("\r\n","<br>",$object->text).'<span class="chat-time">'.$object->time.'</span></p><img src="'.$object->imgPath.'"></div>';
}else{
// 第一回目
$result = '<div class="'.$object->person.'"><p class="chat">'.str_replace("\r\n","<br>",$object->text).'<span class="chat-time">'.$object->time.'</span></p><img src="'.$object->imgPath.'"></div>';
}
}
}
格納時にデータをエンコードしていたので取得する際はデコード(複合化)して使えるデータに変換します。phpのオブジェクトを扱う時のようにデータの下層へと掘り進め、必要なデータを元に変数$resultの中にHTML文書を組み立てておきます。
変数$resultに格納する際はisset($result)
で中身が空かどうかを確認します。ここで7行目のif文を無くすと初回のみ変数$resultが空の状態のまま参照しようとしてNotice: Undefined variable: result
のようなエラーが出てしまいます。
JSONファイルからデータを取り出す時のコツもこちらの記事にまとめてあります。
関連記事:PHPでJSONファイルの扱い方を徹底解説!作成、追記、読込などの基本的な動作と注意するべきポイントとは?
あとは好きなところに$result
を表示させるだけなので入力フォームの上に表示させておきます。
chat-system
の中に新しいグループchat-box
を作りその中にチャットを表示するエリア(chat-area
)と最初に作成したフォームを入れ込んでおきます。
<div class="chat-system">
<div class="chat-box">
<div class="chat-area" id="chat-area">
<?php echo $result; ?>
</div>
<!-- 最初の入力フォーム -->
<form class="send-box flex-box" action="chat.php#chat-area" method="post">
<textarea id="textarea" type="text" name="text" rows="1" required placeholder="message.."></textarea>
<input type="submit" name="submit" value="送信" id="search">
<label for="search"><i class="far fa-paper-plane"></i></label>
</form>
<!-- 最初の入力フォーム -->
</div>
</div>
ここまで作成できたら一度ファイルを開いてみましょう。そのまま開いてもphpは動作しないのでローカル環境でURLを指定して開きましょう!MAMPでいうと「htdocs」の中に今回作成しているchatファイルを置いて「http://localhost/chat/chat.php」と指定すれば開けるはずです。
CSSファイルを読み込んでいれば下画像のようになるはずです。この時点でもチャット機能は正常に動作するので試してみてください。
チャットを送信するともしかしたら「このサイトにアクセスできません」と出るかもしれません。先ほども説明した通り$_SERVER
を使ったheader関数
が上手く動作しないことがありますがもう一度URLを読み込めば正常に動きます。またこれはローカルのみでの挙動で実際にサーバーにアップロードしたファイルではこの現象は起きませんでした。
今回やったこと
長くなってきたので続きは次回にしていきます。その前に今回やったことをおさらいしておきましょう!
- HTMLでの文書基盤作成
- CSSでのデザイン
- 入力値をPOSTで処理
- JSONファイルに格納
- JSONファイルから取得し表示
ここまでで半分ほど作業が完了しました!次にやらなければいけないことは以下の通りです!
- ユーザーを切り替えたフラグの生成
- ユーザーを切り替えた時のデータ格納の分岐
- チャットログのリセット機能
- Topページに戻るときにセッションを破棄
第2回:【第2回】PHPでチャット機能の作り方!ユーザー切り替えの分岐と適切な終了処理
第3回:【Ajax】チャットログを自動更新して表示させる方法!phpとjavascriptで実装
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。
CSSファイルの中身
いかにCSSを載せておきます。コピペしてご使用ください。
@charset "utf-8";
html {
font-size: 62.5%;
}
body {
font-size: 1.7rem;
margin: 0;
}
.text-center {
text-align: center;
}
.chat-box {
border: 5px solid;
width: 80%;
margin: 30px auto;
background-color: rgba(51, 51, 51, 0.208);
}
.chat-box #textarea {
resize: none;
width: 100%;
height: 40px;
line-height: 40px;
border-width: 2px;
margin: 30px 0;
font-size: 2.0rem;
}
.send-box {
text-align: center;
padding: 0 40px;
}
.chat-area {
max-height: 60vh;
padding: 30px 60px;
overflow-y: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
}
.chat-area::-webkit-scrollbar {
display: none;
}
.chat-system input[type="submit"] {
display: none;
}
.change-person {
text-align: center;
}
.change-person .on {
background-color: rgb(230, 196, 83);
}
.change-person i:hover {
font-size: 6.0rem;
transition: all 0.3s;
color: rgb(230, 196, 83);
}
.change-person img,
.change-person i {
transition: all 0.3s;
width: 80px;
height: 80px;
font-size: 5.0rem;
padding: 10px;
border: 5px solid #333;
border-radius: 50%;
line-height: 80px;
text-align: center;
cursor: pointer;
margin: 20px;
overflow: hidden;
}
.send-box label {
margin: 30px 0;
}
.send-box .far.fa-paper-plane::before {
font-size: 3.5rem;
line-height: 44px;
cursor: pointer;
display: inline-block;
width: 200px;
height: 44px;
border: 2px solid;
background-color: #fff;
text-align: center;
vertical-align: bottom;
transition: all 0.3s;
}
.far.fa-paper-plane:hover::before {
background-color: rgb(58, 157, 124);
transition: all 0.3s;
color: #fff;
}
.second .far.fa-paper-plane:hover::before {
background-color: rgb(194, 97, 97);
}
.chat-area .person1 {
text-align: right;
vertical-align: top;
}
.chat-area .person2 {
text-align: left;
position: relative;
}
.second .chat-area .person1 {
text-align: left;
position: relative;
}
.second .chat-area .person2 {
text-align: right;
position: initial;
}
.chat-area .chat {
position: relative;
margin: 20px;
background-color: #fff;
border-radius: 5px;
padding: 15px;
display: inline-block;
max-width: 300px;
text-align: left;
word-wrap: break-word;
}
.chat-area .chat::after {
content: '';
display: inline-block;
width: 0;
border-top: #fff solid 15px;
border-left: transparent solid 6px;
border-right: transparent solid 6px;
position: absolute;
transform: rotate(-120deg);
right: -9px;
bottom: 20px;
}
.chat-area .person2 .chat::after {
border-top: #fff solid 15px;
border-left: transparent solid 6px;
border-right: transparent solid 6px;
position: absolute;
transform: rotate(120deg);
right: initial;
left: -9px;
}
.second .chat-area .chat::after {
transform: rotate(120deg);
right: initial;
left: -9px;
}
.second .chat-area .person2 .chat::after {
transform: rotate(-120deg);
left: initial;
right: -9px;
bottom: 20px;
}
.chat-area .chat-time {
position: absolute;
left: -60px;
}
.chat-area .person2 .chat-time {
left: initial;
right: -60px;
}
.second .chat-area .chat-time {
left: initial;
right: -60px;
}
.second .chat-area .person2 .chat-time {
right: initial;
left: -60px;
}
.chat-area img {
width: 60px;
height: 60px;
border-radius: 50%;
border: 5px solid #fff;
margin: 20px 0;
vertical-align: bottom;
}
.chat-area .person1 img {
background-color: aquamarine;
}
.chat-area .person2 img {
background-color: rgb(194, 97, 97);
}
.chat-area .person2 .chat {
margin-left: 80px;
}
.second .chat-area .chat {
margin-left: 80px;
}
.chat-area .person2 img {
position: absolute;
left: -10px;
bottom: 0px;
}
.second .chat-area img {
position: absolute;
left: -10px;
bottom: 0px;
}
.second .chat-area .person2 img {
position: initial;
}
.btn {
display: block;
width: 200px;
margin: 20px auto;
padding: 15px;
font-size: 1.4rem;
cursor: pointer;
border: none;
background-color: #f5f2f0;
}
.btn:hover {
opacity: 0.8;
}
.flex-box {
display: flex;
justify-content: center;
}
form {
text-align: center;
}
@media (max-width:768px) {
.chat-box {
width: 100%;
margin: 30px auto;
padding: 20px 0;
border-left: none;
border-right: none;
}
.chat-area {
height: 100vh;
padding: 0px 15px;
}
.send-box {
padding: 0 5px;
}
.send-box label {
margin: 0;
}
.chat-system .send-box.flex-box {
width: 100%;
padding: 10px 0px 30px;
flex-direction: row;
align-items: center;
gap: 0 !important;
}
.chat-box #textarea {
width: 70%;
margin: 0;
font-size: 1.3rem;
line-height: 18px;
min-height: 18px;
padding: 4px;
}
.send-box .far.fa-paper-plane::before {
width: 60px;
height: 30px;
margin: 0;
line-height: 30px;
font-size: 1.6rem;
}
.chat-area .chat {
margin: 10px 15px;
padding: 10px;
max-width: 200px;
word-wrap: break-word;
font-size: 1.3rem;
}
.chat-area img {
width: 30px;
height: 30px;
margin: 10px 0;
border-width: 2px;
}
.chat-area .chat::after {
bottom: 10px;
}
.second .chat-area .person2 .chat::after {
bottom: 10px;
}
.chat-area .person2 .chat {
margin-left: 45px;
}
.second .chat-area .chat {
margin-left: 45px;
}
.chat-area .chat-time {
left: -45px;
}
.chat-area .person2 .chat-time {
right: -45px;
}
.second .chat-area .chat-time {
right: -45px;
}
.second .chat-area .person2 .chat-time {
left: -45px;
}
}