PHP＋LINE Messaging APIでクイズアプリを作ってみよう

##今回やりたいこと
今回はPHPでLINE Messaging APIを使いクイズアプリを作成します。

######仕様
・リッチメニューをタップしクイズを出題
・4つの選択肢から回答する
・正解の場合のみ解説の参考サイトurlを添付
・クイズはDB管理とし、クイズの追加・編集・削除機能がある管理画面の作成
※管理画面は後日公開予定です。

こちらから友達追加してお試しいただけます！
https://lin.ee/3AOdmRRlo
<img width="217" alt="スクリーンショット 2020-05-24 13.56.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640991/e83e8252-5a7a-0143-1d5d-c87c151b0188.png">


##イメージ
あいさつメッセージ
<img width="250" alt="IMG_6287.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640991/1e7283e3-539a-2051-9c42-2ada34e01f61.png">


リッチメニューをタップするとクイズを出題
<img width="250" alt="IMG_6306.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640991/a0006d3b-6b08-05c2-ebfa-e0d42cd50bfe.png">


不正解の選択肢をタップした場合のメッセージ
<img width="250" alt="IMG_6289.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640991/1274e4be-5a8f-1a06-ead4-b1e1496ea913.png">


正解の選択肢をタップした場合のメッセージ
<img width="250" alt="IMG_6290.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640991/6e5d794f-10cc-8cd8-9ce9-bef7e63d8f3a.png">


いいえをタップした場合のメッセージ
<img width="250" alt="IMG_6305.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640991/45e77c34-1784-9d20-bf21-644524b734a3.png">


はいをタップした場合、次のクイズが出題
<img width="250" alt="IMG_6306.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640991/713608e9-3e46-1076-06a3-e87ad8b414a2.png">




以上の仕様を実装します。


##VPSで環境構築
VPSはこちらを参考に構築しました。詳細は下記サイトから確認してみてください。他にも参考になる記事がたくさんあるのでググってみて下さい！
[ネコでもわかる！さくらのVPS講座 〜第一回：VPSてなんだろう？〜
](https://knowledge.sakura.ad.jp/7938/)

##Messaging APIを設定
[Messaging APIの概要はこちら](https://developers.line.biz/ja/docs/messaging-api/overview/)

##プログラム


######DBカラム
quizzes : id | quiz | ans1 | ans2 | ans3 | ans4 | correct | url | deleted 

quiz:問題文
ans1~4：選択肢1~4
correct:正解番号
url:解説サイトのurl
deleted:論理削除用

```php:index.php
<?php

$accessToken = 'アクセストークン'; 
//file_get_contents()関数でPOSTされたJSON文字列を取り出し
$jsonString = file_get_contents('php://input'); 
//エラーログ記録
error_log($jsonString); 
//json_decode()関数でJSONをデコードして変数に格納
$jsonObj = json_decode($jsonString); 
//メッセージイベントを取得
//スタンプ、テキスト、画像などの場合がある
$message = $jsonObj->{"events"}[0]->{"message"};
//ReplyToken取得
//受信したイベントに対して返信を行うために必要
$replyToken = $jsonObj->{"events"}[0]->{"replyToken"};
//メッセージタイプ取得
//ここで、受信したメッセージがテキストか画像かなどを判別
$type = $jsonObj->{'events'}[0]->{'type'};

// 送られてきたメッセージの中身からレスポンスのタイプを選択 

//$typeがmessageの場合
if ($type == 'message') {
　　　//messageがtextであり、かつtextがクイズの場合クイズを返信する
	if ($message->{"text"} == 'クイズ') {
　　　　//DB接続
		$dbh = new PDO("mysql:host=localhost;dbname=DB名", 'ユーザー名', 'パスワード');
　　　　　//sql文を変数にセット
    　  //DBから論理削除されていないクイズIDの配列を作る
		$sql = "SELECT id FROM quizzes WHERE deleted = 0";
		$res = $dbh->query($sql);
		$quizIdArray = array();
		foreach ($res as $row) {
			array_push($quizIdArray, $row['id']);
		}
　　　　//クイズIDをランダムに並べ替える
		shuffle($quizIdArray);
    　 //クイズID配列の先頭要素をキーに、クイズを取得し表示する
		$targetQuizId = $quizIdArray[0];
		$sql2 = "select * from quizzes where id = $targetQuizId";
		$stmt = $dbh->query($sql2);
		$targetRow = $stmt->fetch();
　　　　//$messageDataにクイズの内容を代入する
		$messageData = [ 
			'type' => 'template', 
			'altText' => 'クイズ', 
			'template' => [ 'type' => 'buttons', 'text' => $targetRow['quiz'], 
			'actions' => [
			[ 'type' => 'postback', 'label' => $targetRow['ans1'], 'data' => 'ans=' . $targetRow['ans1'] . '&num=1&correct=' . $targetRow['correct'] . '&id=' . $targetRow['id'] ],
			[ 'type' => 'postback', 'label' => $targetRow['ans2'], 'data' => 'ans=' . $targetRow['ans2'] . '&num=2&correct=' . $targetRow['correct'] . '&id=' . $targetRow['id'] ],
			[ 'type' => 'postback', 'label' => $targetRow['ans3'], 'data' => 'ans=' . $targetRow['ans3'] . '&num=3&correct=' . $targetRow['correct'] . '&id=' . $targetRow['id'] ],
			[ 'type' => 'postback', 'label' => $targetRow['ans4'], 'data' => 'ans=' . $targetRow['ans4'] . '&num=4&correct=' . $targetRow['correct'] . '&id=' . $targetRow['id'] ],
			] 
			]
		]; 
　　　　//messageがtextであり、かつtextがいいえであった場合の返信を$massageDataに代入
	}elseif ($message->{"text"} == 'いいえ') {
		$messageData = [ 'type' => 'text', 'text' => 'いつも[解剖学クイズbot]をご利用いただきありがとうございます。クイズチャレンジお疲れ様でした！楽しんでいただけましたでしょうか？またのチャレンジをお待ちしております！' ]; 
		$messageData2 = [ 'type' => 'text', 'text' => '再度クイズにチャレンジする場合は、「クイズにチャレンジ！」をタップしてください！' ]; 
	} else {
　　　//$typeが上記以外の場合の返信内容を$messageDataに代入
		$messageData = [ 'type' => 'text', 'text' => '申し訳ありません、その操作には対応しておりません。「クイズにチャレンジ！」をタップしてクイズにチャレンジしてみて下さい。' ]; 
	} 
}
//$typeがpostbackの場合
if ($type == 'postback') {
　　　// JSONデータからポストバックデータを取得
	$postback = $jsonObj->{"events"}[0]->{"postback"}->{"data"};
　　　//文字列を名前と値に分解し変数に代入
	parse_str($postback, $data);
　　　//各値を変数に代入
	$answer = $data["ans"];
	$number = $data["num"];
	$id = $data["id"];
	$correct = $data["correct"];
　　　//ユーザーの選択した選択肢と正解番号を比較し、正解・不正解の場合で返信内容を$massageDataに代入
　　　//正解の場合
	if ($number == $correct) {
		$dbh = new PDO("mysql:host=localhost;dbname=DB名", 'ユーザー名', 'パスワード');
　　　//選択された選択肢のIDから解説サイトのurlを取得
		$sql3 = "SELECT url FROM quizzes WHERE id = $id";
		$stmt2 = $dbh->query($sql3);
		$targetRowUrl = $stmt2->fetch();
		$url = $targetRowUrl['url'];
　　　//$massageDataに返信内容を代入
		$messageData = [ 'type' => 'text', 'text' => $answer . 'ですね。' ]; 
		$messageData2 = [ 'type' => 'text', 'text' => '素晴らしい！『正解』です！！！' ]; 
		$messageData3 = [ 
			'type' => 'template',
			'altText' => '解説', 
			'template' => [
			'type' => 'buttons',
			'title' => '解説です！',
			'text' => '確認してみてね！', 
			'actions' => [
			[
			'type' => 'uri',
			'label' => 'Wikipediaへ移動', 
			'uri' => "$url", 
			]
			]
			] 
		]; 
		$messageData4 = [ 
			'type' => 'template', 
			'altText' => '次のクイズにチャレンジしますか？', 
			'template' => [ 'type' => 'confirm', 'text' => '次のクイズにチャレンジしますか？', 
			'actions' => [
			[ 'type' => 'message', 'label' => 'はい', 'text' => 'クイズ' ],
			[ 'type' => 'message', 'label' => 'いいえ', 'text' => 'いいえ' ], 
			] 
			]
		];
	}else{
　　　//不正解の場合の返信内容を$massageDataに代入
		$messageData = [ 'type' => 'text', 'text' => $answer . 'ですね。' ]; 
		$messageData2 = [ 'type' => 'text', 'text' => 'むむ、、『不正解』です。' ]; 
	}
}
//メッセージの数に合わせ条件分岐
if ($messageData4 == true) {
	$response = [ 'replyToken' => $replyToken, 'messages' => [$messageData,$messageData2,$messageData3,$messageData4] ]; 
}elseif ($messageData3 == true) {
	$response = [ 'replyToken' => $replyToken, 'messages' => [$messageData,$messageData2,$messageData3] ]; 
}elseif ($messageData2 == true) {
	$response = [ 'replyToken' => $replyToken, 'messages' => [$messageData,$messageData2] ]; 
}else{
	$response = [ 'replyToken' => $replyToken, 'messages' => [$messageData] ]; 
}
error_log(json_encode($response)); 
//curlセッション初期化。urlも設定
$ch = curl_init('https://api.line.me/v2/bot/message/reply'); 
//curlオプション設定
curl_setopt($ch, CURLOPT_POST, true); 
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($response)); 
curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'Content-Type: application/json; charser=UTF-8', 'Authorization: Bearer ' . $accessToken )); 
//curl実行
$result = curl_exec($ch);
//エラーログ記録
error_log($result); 
//curlセッション終了
curl_close($ch);

?>
```

##工夫した点
・クイズをDB管理とし、追加・編集・削除機能をつけた
・クイズをランダムに出題
・ユーザーからのレスポンスに応じて異なる処理

##改善案
・メッセージの数に合わせた条件分岐がダサいのでスマートに書けないものか。。。
・クイズ内容を身体の部位ごとに指定するなど、分野を分ける
・解説を外部サイトに依存しているが、bot内で完結しても良いか
・その場合、画像付きの解説などを入れるとUXが高められそう