a-blog cmsでクイックアンケート機能を構築する

![enquete_preview.gif](https://qiita-image-store.s3.amazonaws.com/0/100753/4e8193ce-286f-fc1b-0cbf-ccd9f8eab9d9.gif)

ブログサイトやポータルサイトで読者にちょっとした意見を聞きたいことがあるかと思います。ワンクリックで参加できるアンケートのようなものがあると読者の参加意識も芽生えます。本稿では少々踏み込んだ内容になりますが、a-blog cmsでクイックアンケート機能を実装する方法をご紹介します。
※データベースを変更する内容が含まれているので、ご利用の場合は自己責任でお願いいたします。

# 実装の流れ

1. a-blog cmsが適用されない領域を作る
2. a-blog cmsが適用されない領域にクイックアンケート用のphpを設置
3. 管理画面にクイックアンケートの設定画面を構築
4. テーマの entry.html にクイックアンケートの回答画面の雛形を設置
5. クイックアンケート用のphpと、entry.htmlに設置したクイックアンケートの雛形を連携

# 1. a-blog cmsが適用されない領域を作る

まずa-blog cms領域直下にある、.htaccessでa-blog cmsが適用されないディレクトリを作成します。今回は、/system/ 以下をa-blog cmsが適用されないディレクトリにします。

## 修正前

```/.htaccess
# a-blog cms以外のコンテンツ（a-blog cmsを動作させないディレクトリ）
# RewriteCond %{REQUEST_URI} !^/?other/
# RewriteCond %{REQUEST_URI} !^/?other2/
```

## 修正後

```/.htaccess
# a-blog cms以外のコンテンツ（a-blog cmsを動作させないディレクトリ）
RewriteCond %{REQUEST_URI} !^/?system/?
```

# 2. a-blog cmsが適用されない領域にクイックアンケート用のphpを設置

`/system/enquete/?mode=get&eid=1` のようなリクエストを送れば回答結果のJSONを返す、`/system/enquete/?mode=put&eid=1&answer=良かった` のようなリクエストを送れば投票してページに戻すphpプログラムを組みます。eidでどの記事のアンケートが回答されたか特定します。

```/system/enquete/index.php
<?php
/**
 * クイックアンケートシステム
 * /system/enquete/?mode=get&eid=1
 * /system/enquete/?mode=put&eid=1&answer=良かった
 */

	require_once('../../config.server.php');
	$dsn = 'mysql:dbname='.DB_NAME.';host='.DB_HOST.';charset=utf8';
	$pdo = new PDO($dsn, DB_USER, DB_PASS);
	$debug = false;

	// テーブルがなければ新規制作
	$sql = "CREATE TABLE IF NOT EXISTS `enquete_table`
		(
			`id` INT PRIMARY KEY AUTO_INCREMENT,
			`entry_id` INT,
			`account_id` VARCHAR(255),
			`answer`  VARCHAR(255),
			`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
		);";
	$stmt = $pdo -> prepare($sql);
	$stmt -> execute();

	// パラメータを受取
	$mode         = filter_input(INPUT_POST, 'mode');
	$eid          = filter_input(INPUT_POST, 'eid');
	$userid       = filter_input(INPUT_COOKIE, 'enquete_userid');
	$answer       = filter_input(INPUT_POST, 'answer');
	$result       = filter_input(INPUT_POST, 'result');

	// Cookieがなければ発行
	if( !$userid && !$empid ){
		$ticket = md5(uniqid(mt_rand(), true));
		$expire = time() + ( 60 * 60 * 24 * 365 ); //1年間間保持
 		setcookie('enquete_userid', $ticket, $expire, '/');
 		$userid = $ticket;
	}
	
	// 投票済みかチェック
	if( $userid ){
		$stmt = $pdo->prepare("SELECT COUNT(*) FROM `enquete_table` WHERE `entry_id` = ? AND `account_id` = ?");
		$stmt->execute([$eid, $userid]);
		$voted = $stmt->fetchColumn();
	}

	// 投票結果
	if( $mode == 'get' && $eid && $voted ){
		// 投票を集計してランキングを降順で出力
		$stmt = $pdo->prepare("SELECT `answer`, SUM(1) AS `sum` FROM `enquete_table` WHERE `entry_id` = ? GROUP BY `answer` ORDER BY `sum` DESC");
		$stmt->execute([$eid]);
		// JSON形式で出力
		$json = array();
		foreach ($stmt as $i => $row) {
			$json[] = array(
				'answer' => $row['answer'],
				'sum' => $row['sum'],
			);
		}
		echo json_encode($json);
		exit();
	}

	// 投票処理
	if( $mode == 'put' && $eid && $userid && $answer ){
		// 投票済みでないかチェック
		$stmt = $pdo->prepare("SELECT COUNT(*) FROM `enquete_table` WHERE `entry_id` = ? AND `account_id` = ?");
		$stmt->execute([$eid, $userid]);
		// 投票済みでなければ投票
		if( $stmt->fetchColumn() == 0 || $debug ){
			$stmt = $pdo->prepare("INSERT INTO `enquete_table` (`entry_id`, `account_id`, `answer`) VALUES (?, ?, ?)");
			$stmt->execute([$eid, $userid, $answer]);
		}
		// 詳細ページへ戻す
		header('location: ' . $result);
		exit();
	}
	
	// 詳細ページへ戻す
	if( $eid ){
		header('location: ' . $result);
	}
	exit();
?>
```


# 3. 管理画面にクイックアンケートの設定画面を構築

クイックアンケートは記事毎に設置できるようにします。ステータスで「表示する」「表示しない」を切り替え、グラフの種類を「ドーナツグラフ」「円グラフ」「棒グラフ」から選択、回答内容はカスタムフィールドグループで増減できるようにします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100753/9423bb21-6043-2e2d-2d25-54ca8a0ebf76.png)

```/themes/*****/admin/field.html
<h3 class="acms-admin-accordion-title"><a href="#adminEntryEnquete" class="js-fader acms-admin-accordion-title-link">クイックアンケート設定 <span class="acms-admin-icon-arrow-small-down acms-admin-margin-left-mini"></span></a></h3>
<div id="adminEntryEnquete" class="acms-admin-accordion-inner">
<table class="acms-admin-table-admin-edit">
  <tr>
    <th>ステータス</th>
    <td>
      <div class="acms-admin-form-radio">
        <input type="radio" name="entry_enquete" value="enabled"<!-- BEGIN_IF [{entry_enquete}/em] --> checked="checked"<!-- END_IF --> {entry_enquete:checked#enabled} id="input-radio-entry_enquete-enabled" />
        <label for="input-radio-entry_enquete-enabled"><i class="acms-admin-ico-radio"></i>表示する</label>
      </div>
      <div class="acms-admin-form-radio">
        <input type="radio" name="entry_enquete" value="disabled"{entry_enquete:checked#disabled} id="input-radio-entry_enquete-disabled" />
        <label for="input-radio-entry_enquete-disabled"><i class="acms-admin-ico-radio"></i>表示しない</label>
      </div>
      <input type="hidden" name="field[]" value="entry_enquete" />
    </td>
  </tr>
  <tr>
    <th>グラフの種類</th>
    <td>
      <div class="acms-admin-form-radio">
        <input type="radio" name="enquete_type" value="doughnut"{enquete_type:checked#doughnut} id="input-radio-enquete_type-doughnut" />
        <label for="input-radio-enquete_type-doughnut"><i class="acms-admin-ico-radio"></i>ドーナツグラフ</label>
      </div>
      <div class="acms-admin-form-radio">
        <input type="radio" name="enquete_type" value="pie"{enquete_type:checked#pie} id="input-radio-enquete_type-pie" />
        <label for="input-radio-enquete_type-pie"><i class="acms-admin-ico-radio"></i>円グラフ</label>
      </div>
      <div class="acms-admin-form-radio">
        <input type="radio" name="enquete_type" value="bar"{enquete_type:checked#bar} id="input-radio-enquete_type-bar" />
        <label for="input-radio-enquete_type-bar"><i class="acms-admin-ico-radio"></i>棒グラフ（縦）</label>
      </div>
      <div class="acms-admin-form-radio">
        <input type="radio" name="enquete_type" value="horizontalBar"{enquete_type:checked#horizontalBar} id="input-radio-enquete_type-horizontalBar" />
        <label for="input-radio-enquete_type-horizontalBar"><i class="acms-admin-ico-radio"></i>棒グラフ（横）</label>
      </div>
      <input type="hidden" name="field[]" value="enquete_type" />
    </td>
  </tr>
  <tr>
    <th>タイトル</th>
    <td>
      <input type="text" name="enquete_title" value="{enquete_title}" class="acms-admin-form-width-full"/>
      <input type="hidden" name="field[]" value="enquete_title" />
    </td>
  </tr>
  <tr>
    <th>リード文</th>
    <td>
      <textarea name="enquete_lead" class="acms-admin-form-width-full">{enquete_lead}</textarea>
      <input type="hidden" name="field[]" value="enquete_lead" />
    </td>
  </tr>
  <tr>
    <th>選択項目</th>
    <td>
      <table class="js-fieldgroup-sortable adminTable">
        <tbody>
          <!-- BEGIN enquete_answer:loop -->
          <tr class="sortable-item">
            <td class="item-handle"><i class="acms-admin-icon-sort"></i></td>
            <td>
              <input type="text" name="enquete_answer_item[{i}]" value="{enquete_answer_item}" class="acms-admin-form-width-full"/>
            </td>
          <td><input type="button" class="item-delete acms-admin-btn-admin acms-admin-btn-admin-danger" value="削除" /></td>
          </tr>
          <!-- END enquete_answer:loop -->
          <tr class="sortable-item item-template">
            <td class="item-handle"><i class="acms-admin-icon-sort"></i></td>
            <td>
              <input type="text" name="enquete_answer_item[]" value="" class="acms-admin-form-width-full"/>
            </td>
            <td><input type="button" class="item-delete acms-admin-btn-admin acms-admin-btn-admin-danger" value="削除" /></td>
          </tr>
        </tbody>
        <tfoot>
          <tr><td colspan="3"><input type="button" class="item-insert acms-admin-btn-admin" value="追加" /></td>
          </tr>
        </tfoot>
      </table>
      <input type="hidden" name="@enquete_answer[]" value="enquete_answer_item" />
      <input type="hidden" name="field[]" value="enquete_answer_item" />
      <input type="hidden" name="field[]" value="@enquete_answer" />
    </td>
  </tr>
</table>
</div>

```


# 4. テーマの entry.html にクイックアンケートの回答画面を設置

記事ページにクイックアンケートの回答画面を設置します。この回答画面で入力した内容をクイックアンケート用のphpに送信します。どの記事のクイックアンケートに回答があったか特定するため、`<input type="hidden" name="eid" value="{entry:loop.eid}">` でエントリーIDを特定します。回答したら記事ページに戻ってきてほしいので、`<input type="hidden" name="result" value="%{PERMALINK}#enquete">`に戻り先URLを指定します。ステータスが「無効」の場合や、クイックアンケートの設定がない場合はこの項目自体表示しません。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100753/77ee3166-9518-d635-0c46-6e3084e557ea.png)

```/themes/*****/entry.html

<!-- BEGIN_IF [{entry_enquete}/neq/disabled/_and_/{enquete_title}/neq/] -->
<div class="entry-enquete" id="enquete" data-enquete-id="%{EID}" data-enquete-type="{enquete_type}" data-enquete-title="{enquete_title}">
  <form action="/system/enquete/" method="post">
    <h2><span class="jp">クイックアンケート</span></h2>
    <dl>
      <dt class="title">{enquete_title}</dt>
      <!-- BEGIN enquete_lead: veil -->
      <dd class="lead">{enquete_lead}[nl2br]</dd>
      <!-- END enquete_lead: veil -->
      <dd class="answer">
      <!-- BEGIN enquete_answer:loop -->
      <label><input type="radio" name="answer" value="{enquete_answer_item}"> {enquete_answer_item}</label>
      <!-- END enquete_answer:loop -->
      </dd>
      <dd class="answer-other"><textarea name="answer_other" class="text-area" placeholder="コメントがあれば自由にご記入ください。（回答結果には表示されません）"></textarea></dd>
    </dl>
    <p><button type="submit" class="button-form">投票する</button></p>
    <input type="hidden" name="mode" value="put">
    <input type="hidden" name="eid" value="{entry:loop.eid}">
    <input type="hidden" name="result" value="%{PERMALINK}#enquete">
  </form>
</div>
<!-- END_IF -->
```


# 5. 投票したらChart.jsでアンケートの回答結果を表示

投票したら /system/enquete/index.php に対して modeにget、eidに記事のIDのパラメータを渡して、アンケート回答結果のJSONを受け取ります。アンケート回答結果のJSONをChart.jsで描画します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100753/1def196b-a125-23db-21f2-ae35e7961af1.png)

```/themes/*****/include/head/js.html
<script src="/shared/scripts/chart-js/Chart.bundle.min.js"></script>
<script src="/shared/scripts/enquete.js"></script>
```

```/themes/*****/js/enquete.js
$(document).ready(function(){

	/*
	/*	クイックアンケート
	*/
	$('.entry-enquete .answer-other').hide();
	$('.entry-enquete input[name="answer"]').change(function(){
		if($('.entry-enquete input[name="answer"]:checked').val().indexOf('その他') != -1){
			$('.entry-enquete .answer-other').slideDown();
		} else {
			$('.entry-enquete .answer-other').slideUp();
		}
	});
	if($('.entry-enquete').size()>0){
		var $this = $('.entry-enquete');
		var url = '/system/enquete/';
		var eid = $this.data('enquete-id');
		var type = $this.data('enquete-type');
		var title = $this.data('enquete-title');
		$this.find('.rom').click(function(){
			enqueteRender($this, 1);
			return false;
		});
		enqueteRender($this);
	}
	function enqueteRender($this, rom){
		$.ajax({
			context: $this,
			type: 'POST',
			url: url,
			cache: false,
			dataType: 'json', 
			data: {
				'mode': 'get',
				'eid': eid
			}
		}).done(function(data){
			if(rom && data.length == 0){
				alert('回答がまだありません');
			}
			if(data.length > 0){
				// HTMLを描画
				var thanks = !rom ? 'ご回答ありがとうございました。' : '';
				$(this).addClass('-result').html('\
					<h2 class="h2e"><span class="en">クイックアンケート</span><span class="jp">'+thanks+'現在の回答結果は以下のとおりです。</span></h2>\
					<div class="inner">\
					<div class="graph -'+type+'"><canvas id="chart"></canvas></div>\
					<div class="text">\
					<p class="title">'+title+'</p>\
					<ul class="legend"></ul>\
					</div>\
					</div>\
				');

				// 変数定義
				var colors = ["#c93a40", "#d16b16", "#a0c238", "#56a764", "#65ace4", "#0074bf", "#9460a0", "#cc528b"],
					labels = new Array(), 
					sums   = new Array(),
					total  = 0;
				// JSONをperse
				for(var i=0; i<data.length; i++){
					labels.push(data[i]['answer']);
					sums.push(data[i]['sum']);
					total += data[i]['sum']*1;
				}
				// 円グラフの場合
				if( type == 'doughnut' || type == 'pie' ){
					// 判例を描画
					for(var i=0; i<data.length; i++){
						$('#enquete .legend').append('<li><span style="background-color:'+colors[i]+'"></span>'+labels[i]+' '+Math.round(sums[i] / total * 100)+'％</li>')
					}
					// グラフを描画
					var chart = new Chart(document.getElementById("chart").getContext("2d"), {
						type: type,
						data: {
							datasets: [{
								data: sums,
								backgroundColor: colors
							}],
							labels: labels
						},
						options: {
							responsiveAnimationDuration: 1000,
							tooltips: {
								bodyFontSize: 10,
								callbacks: {
									label: function(tooltipItem, data) {
										var label = data.labels[tooltipItem.index];
										return label;
									},
									footer: function(tooltipItem, data) {
										var value = data.datasets[0].data[tooltipItem[0].index];
										var percentage = Math.round(value / total * 100);
										return percentage + '%';
									}
								}
							},
							legend: {
								display: false
							},
						}
					});
				}
				// 棒グラフの場合
				if( type == 'bar' || type == 'horizontalBar' ){
					// 判例を描画
					for(var i=0; i<data.length; i++){
						$('#enquete .legend').append('<li><span style="background-color:'+colors[i]+'"></span>'+labels[i]+' '+sums[i]+'件</li>')
					}
					// グラフを描画
					if( type == 'bar' ){
						var chart = new Chart(document.getElementById("chart").getContext("2d"), {
							type: type,
							data: {
								datasets: [{
									data: sums,
									backgroundColor: colors
								}],
								labels: labels
							},
							options: {
								responsiveAnimationDuration: 1000,
								tooltips: {
									bodyFontSize: 10
								},
								scales: {
									yAxes: [{
										ticks: {
											min: 0
										}
									}],
									xAxes: [{
										display: false
									}]
								},
								legend: {
									display: false
								}
							}
						});
					}
					// グラフを描画
					if( type == 'horizontalBar' ){
						var chart = new Chart(document.getElementById("chart").getContext("2d"), {
							type: type,
							data: {
								datasets: [{
									data: sums,
									backgroundColor: colors
								}],
								labels: labels
							},
							options: {
								responsiveAnimationDuration: 1000,
								tooltips: {
									bodyFontSize: 10
								},
								scales: {
									xAxes: [{
										ticks: {
											min: 0
										}
									}],
									yAxes: [{
										display: false
									}]
								},
								legend: {
									display: false
								}
							}
						});
					}
				}
			}
		});
	}

});
```

# さいごに
これでクイックアンケートは完成です。動的フォームでは制限がありますが、カスタムフィールドとphpを組み合わせることで、a-blog cmsの魅力的な管理画面のUIを使って、運営者に優しいシステムの構築が可能です。