Wordpressで会員制サイトを構築してみよう（ユーザープロフィールをAjaxで更新させる）


[ユーザー項目の変更 その2](http://qiita.com/hiroki_y/items/2e8a0d36c2e0c8f819fd)で、プロフィールページを整え、ユーザーの情報を更新できるようにしました。

今回は、もっと柔軟なサービス構築ができるよう **「任意のページからAjaxでユーザープロフィール更新（プラグイン未使用）」**を実装してみます。結構長いです。

##元記事

<a href="http://www.rokurofire.info/2016/05/28/wp_membership10/">WordPressで会員制サイトを構築してみよう（ユーザープロフィールをAjaxで更新させる） - rokuro Fire</a>

##完成予想

こんな感じです。本記事ではチェックボックス形式のものを想定しています。

![movie.gif](https://qiita-image-store.s3.amazonaws.com/0/10172/18c7d49c-1ca3-9bd2-55bd-6eb82ac23c60.gif "movie.gif")

##更新させたいユーザー項目

以下、チェックボックスの ON/OFF をユーザーに切り替えてもらうという想定です。

▼あなたの持っているスキル（user_skills）

* Webデザイン（user_skill_design）：true or false
* コーディング（user_skill_coding）：true or false
* フロントエンド開発（user_skill_js）：true or false
* WordPress制作（user_skill_wp）：true or false

各項目の値は true or false になります。

##前準備

Ajaxでポストさせる際、nonceの値やAjaxのアクセス先URLを、事前にJavaScriptで扱えるようにしておく必要があります。
以下、base.jsという自前のJavaScriptファイルを読み込む前提で記述しています。

```functions.php
function my_wp_enqueue_scripts() {
	wp_enqueue_script('basejs', get_template_directory_uri() . '/js/base.js', array(), null, true);
	
	$parameters = array (
		'ajaxurl' => admin_url('admin-ajax.php'),
		'nonce' => wp_create_nonce('user_nonce')
	);
	wp_localize_script('basejs', 'myWPObj', $parameters);
}
add_action( 'wp_enqueue_scripts', 'my_wp_enqueue_scripts', 100);
```

上記を記述すると、wp_footer(); に以下が出力されます。

```
<script type='text/javascript'>
/* <![CDATA[ */
var myWPObj = {"ajaxurl":"{admin-ajax.phpへのフルパス}\/admin-ajax.php","nonce":"{ランダムな文字列}"};
/* ]]> */
</script>
<script type='text/javascript' src='{base.jsへのフルパス}/js/base.js'></script>
```

"myWPObj"というJavaScriptのグローバル変数が作成され、そのプロパティとして、admin-ajax.phpへのフルパスとnonceの値が格納されるようになります。

####（余談）非同期読み込みさせたい場合

上記で出力されるbase.jsを非同期読込させたい場合は、以下の様な方法があるようです。

```
/* base.js に async/defer を追加 */
function my_script_loader_tag($tag, $handle) {
	if ("basejs" !== $handle ) {
		return $tag;
	}
	return str_replace(" src", " async defer src", $tag);
}
add_filter("script_loader_tag", "my_script_loader_tag", 10, 2);
```

$handleの値が"basejs"ではないときに、" src"を" async defer src"に置換しているだけですね。
ちなみに$handleは、wp_enqueue_script('basejs',〜〜〜) で定義した第1引数の値です。

##更新させたいユーザー項目を定義

functions.php に、下記のように項目を定義します。

```functions.php
// チェックリスト定義
function define_checklist($name) {
	$checklist = array(
		'user_skills' => array(
			'label' => 'あなたの持っているスキル',
			'lists' => array(
				'user_skill_design' => array(
					'label' => 'Webデザイン',
				),
				'user_skill_coding' => array(
					'label' => 'コーディング',
				),
				'user_skill_js' => array(
					'label' => 'フロントエンド開発',
				),
				'user_skill_wp' => array(
					'label' => 'WordPress制作',
				)
			)
		)
	);

	return $checklist[$name];
}
```

各ユーザー項目をオブジェクト形式にしておけば、label以外の要素を後から増やすことができます。

また、チェックリストの種類が増えた時には以下のように増やします。

```
$checklist = array(
	'user_skills' => array(...),
	'hogehoge_check1' => array(...),
	'hogehoge_check2' => array(...),
);
```

##データを保存する処理を作成

Ajaxでポストされたデータを、データベースに保存する処理を作成します。
今回、配列形式のデータを1つのカラムに保存するようにしみます。

### DBに保存する値の形式

チェックリスト保存形式は配列です。

```
// チェックリスト保存形式は配列
array(
	"user_skill_design" => "false",
	"user_skill_coding" => "true",
	"user_skill_js" => "true",
	"user_skill_wp" => "false"
);
```

### JavaScriptからポストされるデータの例

ajaxでチェックボックスを切り替える度にデータがポストされる仕様の場合です。
今回は切り替えた1件の状態のみをポストしています。

```
"user_skill_design=false"
```

###ポストされたデータの保存処理

上記を仕様を踏まえて、以下のような処理を作成します。
以下URLを参考にしています。

* [Trying to update User Meta with Ajax](http://wordpress.stackexchange.com/questions/208907/trying-to-update-user-meta-with-ajax)

```functions.php
function my_user_profile_update_checklist() {
	// ポスト出来るのはログイン中のユーザーのみ
	if (is_user_logged_in()) {
		if(empty($_POST) || !isset($_POST)) {
			ajaxStatus('error', 'Nothing to update.');
		} else {
			$data = $_POST;
			// ポストされたチェックリストのデータ
			$dataString = $data['post'];	

			// アクションの名前（今回は "submit_checklist"）
			$dataAction = $data['action'];

			// ユーザー項目の名称：user_skills
			$dataType = $data['type'];

			// ポストされたデータをパースし、変数$dataArrayに格納
			parse_str($dataString, $dataArray);

			// バリデーションチェックを行う場合はここで
			// my_validation_check($dataArray);

			// nonceの値をチェック
			$nonce = $data['nonce'];
			if(wp_verify_nonce($nonce, 'user_nonce') !== false) {
				$user_ID = get_current_user_id();
				
				// アクション名のチェック
				if ($dataAction == 'submit_checklist') {
					// ポストしたユーザーのチェックリストを取得
					$user_skills = get_user_meta(get_current_user_id(), $dataType, true );
					
					$user_meta_key;
					$user_meta_value;
					// ユーザーIDの存在チェック
					if($user_ID != NULL) {
						foreach($dataArray as $key => $value) {
							$user_meta_key = $key;
							$user_meta_value = $value;
						}

						// user_skillsの、変更の合った項目の値を変更
						$user_skills[$user_meta_key] = $user_meta_value;

						// ユーザーのプロフィールをアップデート
						$status = update_user_meta($user_ID, $dataType, $user_skills);

						ajaxStatus('success', 'Meta fields updated.', $dataArray);
					} else {
						ajaxStatus('error', 'No user.', $dataArray);
					}
				} else {
					ajaxStatus('error', 'Unauthorized action.', $dataArray);
				}
			} else {
				ajaxStatus('error', 'Nonce error.');
			}
		}
	}
}

// レスポンスデータの作成
function ajaxStatus($status, $message, $data = NULL) {
	$response = array (
		'status' => $status,
		'message' => $message,
		'data' => $data
	);
	
	// キャッシュさせない
    nocache_headers();
    $charset = get_bloginfo("charset");
    header( "Content-Type: application/json; charset=" . $charset);
    echo json_encode($response);
    die();
}

add_action( 'wp_ajax_submit_checklist', 'my_user_profile_update_checklist');
```

以下の順番で処理を行っています。

* ポストされたデータをパース
* nonceの値、アクション名、ユーザーの存在をチェック
* ユーザーのDBに保存されているチェックリストを取得
* ユーザーのチェックリストの値を変更
* ユーザーのプロフィールをアップデート
* レスポンスデータを作成し、結果を返す

パースの処理を行うと、ポストされた各データは以下のようになります。

```
// $dataString：ポストされたデータの値
"user_skill_design=false"

// ↓↓↓
// パースする
parse_str($dataString, $dataArray);

// ↓↓↓
// オブジェクトになる（$dataArrayの値）
array("user_skill_design" => "false");
```

今回はポストされるデータが true or false のみという前提ですが、任意のテキストを入力させる場合は、"my_validation_check" のあたりでバリデーションのチェックを行う必要があります。

##テンプレートの作成

更新させたい固定ページとテンプレートファイルを作成します。
以下はチェックボックスの出力です。

```page-xxxx.pnp
<?php
// 定義したリストを取得
$checklist = define_checklist('user_skills');

// DBに保存されているユーザーのデータを取得
$user_checklist_skills = get_user_meta(get_current_user_id(), 'user_skills', true);

// チェックボックスの出力
$html = '';
foreach ($checklist["lists"] as $key => $value) {
	if (!empty($user_checklist_skills[$key])) {
		$checked = ($user_checklist_skills[$key] == "true") ? "checked" : "";
	} else {
		$checked = "";
	}
	$html .= '<div class="checkwrap">'
			. '<form id="form_' . $key . '" method="POST">'
			. '<input type="checkbox" name="' . $key . '" class="checkinput" id="' . $key . '" ' . $checked .'>'
			. '<label for="' . $key . '" class="checklabel"><span class="ind"></span></label>'
			. '</form></div>'
			. '<p>' . $value["label"] . '</p>'
			. '</div>';
}
echo $html;
?>
```

データが空の時はチェックボックスにチェックが入らないような分岐が必要です。

## JavaScriptでポスト

```base.js
// Ajax用のオブジェクトを作っておくと便利かも？
var AjaxObj = function (obj) {
    this.url = obj.url;
    this.data = obj.data;
    this.action = obj.func;
	this.action_false = obj.func_false;
	this.type = obj.type;
};
AjaxObj.prototype.call = function () {
    var action = this.action,
		action_false = this.action_false;
    jQuery.ajax({
        type: this.type,
        url: this.url,
        data: this.data
    }).done(function (data, status, xhr) {
        action(data, status, xhr);
    }).fail(function (e) {
		action_false();
		return false;
    }).always(function () {
		return true;
    });
};

// チェックボックスにchangeイベント追加
jQuery("input.checkinput").on('change', function (e) {
	var self = jQuery(this),
		self_checked = self.is(':checked'),
		label = self.parent().children("label.checklabel"),
		postobj = "";
	
	// チェックボックス変更後、一定時間変更不可
	self.attr("disabled", true);
	label.addClass('loading');
	e.preventDefault();
	
	// ポストするデータを作成
	postobj = self.attr("name") + "=" + self_checked;
	
	// ポストした後のアクション
	var resAction = function (data, status, xhr) {
		if (data.status === "success") {
			label.removeClass('loading');
			self.attr("disabled", false);
		} else {
			resAction_false();
			return false;
		}
	};
	
	// レスポンスエラーの時
	var resAction_false = function () {
		label.removeClass('loading');
		self.attr("disabled", false);
		self.prop('checked', !self_checked);	// チェック状態を元に戻す
		return false;
	};
	
	var getInfo = new AjaxObj({
		url: myWPObj.ajaxurl,
		data: {
			action: 'submit_checklist',
			nonce: myWPObj.nonce,
			type: 'user_skills',
			post: postobj
		},
		func: resAction,
		func_false: resAction_false,
		type: 'POST'
	});
	getInfo.call();
});
```

これで、ひとまずポストに関する処理は完成（のはず）です。

## データベースの値を確認

実際に保存されたデータを見てみると、1件のレコードにシリアライズされて格納されています。
保存されているテーブルは wp_usermeta です。

```
mysql> select meta_value from wp_usermeta where meta_key="user_skills";

+--------------------------------------------------------------------------------+
| meta_value                                                                     |
+--------------------------------------------------------------------------------+
| a:2:{s:17:"user_skill_design";s:4:"true";s:17:"user_skill_coding";s:4:"true";} |
+--------------------------------------------------------------------------------+

```

## レスポンスが返ってくるまでローディング

![movie.gif](https://qiita-image-store.s3.amazonaws.com/0/10172/18c7d49c-1ca3-9bd2-55bd-6eb82ac23c60.gif "movie.gif")

上記で処理は完成ですが、レスポンスが返ってくるまでの間、設定変更が完了したかどうかが分かりづらいのが問題です。

以下のような、クオリティの高いサンプルスピナーを流用させてもらいましょう。

* [http://projects.lukehaas.me/css-loaders/](http://projects.lukehaas.me/css-loaders/)