WordPressのプラグインシステムはわりと充実しているので、テンプレートタグとフックを組み合わせれば結構簡単に作れます。ただ、データベースを追加して云々するのは難しくはなくてもちょっと面倒くさいので、やり方をまとめておきます。全部で4時間ぐらいでした。
今回設計するプラグインの概要
まず、ほしいと思った機能を確定します。
Facebookのヘルプセンターには「このヘルプは役に立ちましたか?」というのがあります。これをミニコme!につけたいなと思いました。ミニコme!はサービスの仕様上、出品者が自力で問題を解決できるようになることで売上が上がるはずなので、ぜひともほしい機能ですね。
どうせ集計するならせっかくなので、Amazonにある「◯人中◯人がこのレビューが参考になったといっています」という機能もつけたいなと思うようになりました。
というわけで、プラグインの概要としては「投稿およびコメントに対するフィードバックを集計するプラグイン」ということになりますね。
車輪の再発明をする前に探してみましたが、「惜しいな」というものをいくつか発見したものの、そのものズバリはありませんでした。というわけで、つくることに決定。
テーブル構造も含めてよく考える
一度つくることに決めたのはまあいいとして、以下のことをよく考えます。
テーブルは新たに必要か
WordPressにはデフォルトで10個のテーブルがあります。機能によっては、このテーブルだけでなんとかなってしまいます。特に下記のテーブルが役立ちます。
- wp_postmeta
- 投稿のメタ情報。投稿IDとmeta_key, meta_valueがある。カスタムフィールドはここに保存されている。
- wp_usermeta
- ユーザーのメタ情報。ユーザーIDとmeta_key, meta_valueがある。ユーザーのプロフィールやホームページのアドレス、リッチエディターを使うか否かはすべてここに保存されている。
- wp_commentmeta
- コメントのメタ情報。コメントID、meta_key, metavalueがある。あんまり使われていないので、普通に使っているとなんにもデータが入らないことがある。たとえば、コメントに☆をつける場合などはこのテーブルを利用する。
上記のテーブルを利用するメリットは以下の2つがあります。
- テーブルの作成・管理が必要ない
- テーブルのCRUD(データの挿入・取得・更新・削除)がはじめからついている
というわけで、独自のテーブルを持つプラグインを作成する場合は、上記のメリットを捨てる理由があるものに限ります。今回のプラグインはあった方がよさそうです。
テーブルの構造を決定する
今回のプラグインのデータ構造はこんな感じにしました。
- 投稿またはコメントに対してひもづく
- 評価には肯定的なものと否定的なものがあり、それぞれ別に集計する
- すべての評価は1としてカウントされ、誰が評価したかは判断しない
テーブルの構造はこんな感じになります。
フィールド名 | データ型 | NULL | その他 | 説明 |
---|---|---|---|---|
ID | int(11) | NOT NULL | AUTO_INCREMENT | データの主キー |
object_id | int(11) | NOT NULL | 紐付ける対象のID | |
post_type | varchar(45) | NOT NULL | 投稿タイプ | |
positive | int(11) | NOT NULL | 肯定的な評価 | |
negative | int(11) | NOT NULL | 否定的な評価 | |
updated | datetime | NOT NULL | 最終更新日時 |
Facebookの「いいね!」みたいに「誰がつけたのか」が重要になる場合は、ユーザーIDが必要になりますので、別の構造になります。ただ、その場合は凄まじい量のデータ更新があるはずなので、Tokyo CabinetとかのKVS使ったほうがいいのかもしれません。
ちなみに、MySQLで急に怯え出すPHPerがたまにいますが、データの正規化について学べばそんなに難しくはないです。二億件データがあるとか、0.1秒でレスポンスする全文検索作れとか、秒間7万レスポンスさばけとか、二分木検索がうんたらかんたらとかは難しいですが、普通のWebサイト作ってるだけならなんとかなります。
プラグイン作成の下準備
さて、準備ができたらプラグインの作成に入ります。プラグインの名前は”Anyway Feedback”としましょう。なんでもいいから、フィードバックを集められるプラグインですね。「同じ人が重複登録したらどうするか」とかはあんまり考えないようにします。
とりあえず、anyway-feedbackというフォルダを作成して、ローカルで動いているwp-content/pluginsに配置します。そして、その中に下記のファイルを配置します。別にこういうファイル構造じゃなくてもいいんですが、特にアイデアがない人はこんな感じにするといいかもしれません。
- anyway-feedback.php…コアファイル(フックの登録などに使う)
- anyway-feedback.class.php…クラスファイル(開発者が使う機能専用)
- functions.php…ユーザー関数(テンプレートタグとしてユーザーが使うものをまとめる)
- assets…JSやCSS用のフォルダ
- languages…言語フォルダ。国際化対応しておいた方が人気出るらしいので、はじめから作っておく。
プラグイン有効化時にテーブルを作成
プラグインは基本的に決まった書式でコメントを書いておけばWordPressに認識されるのですが、データベースを管理できるようにしておく必要があります。プラグインは有効化・無効化されることがあり、気に入らなければ削除されることがあります。毎回データベースがあるかないかをチェックするのも面倒なので、「プラグインが有効化されたときにデータベースがなければ作成し、あっても構造が古ければ更新する」という仕様にしましょう。
幸い、WordPressにはこのためのお作法が決まっています。
アクチベーションフックの登録
まず、anyway-feedback.phpにこんな感じでコメントを書きます。
<?php /* Plugin Name: Anyway Feedback Plugin URI: http://hametuha.co.jp/ Description: Help to assemble simple feedback(negative or positive) and get statics of them. Version: 0.1 Author: Takahashi Fumiki (Hametuha inc.) Author URI: http://hametuha.co.jp Copyright 2011 hametuha(email : info@hametuha.co.jp) TextDomain: anyway-feedback /*これでとりあえずは動きます。あとは必要なファイルを読み込み。
//Load required files require_once dirname(__FILE__).DIRECTORY_SEPARATOR."anyway-feedback.class.php"; require_once dirname(__FILE__).DIRECTORY_SEPARATOR."widgets".DIRECTORY_SEPARATOR."popular.php"; require_once dirname(__FILE__).DIRECTORY_SEPARATOR."functions.php";インスタンスを初期化し、プラグイン有効化時に実行されるフックを登録します。
//Make Instance global $afb; $afb = new Anyway_Feedback(); //Register Activation Hook. register_activation_hook(__FILE__, array($afb, "activate"));これで、プラグイン有効化時にAnyway_Feedbackの関数activateが実行されるようになります。注意点としては、プラグイン有効化前はプラグインファイルに書いた変数がグローバルスコープにならないので、global $afb;とグローバル宣言することですかね。
ためしにanyway-feedback.class.phpにこんな風に書いてみましょう。class Anyway_Feedback{ function activate(){ echo "Activated!"; die(); //プログラム終了 } }これで有効化すると、管理画面が真っ白になるはずです。ビックリしましたね。びっくりした後は関数を空っぽにして再読み込み、安心しましょう。。これでアクチベーションフックは登録でき、「プラグインを有効化したときに関数が実行できる」ようになりました。
テーブルの作成・管理
WordPressにはdbDeltaという知る人ぞ知る関数があり、「テーブルが古ければ更新し、新しければアップデートする」という機能を司っています。超おりこうな関数ですね。
上記で決定したテーブルを作成するSQL文は下記の通りです。
CREATE TABLE wp_afb_feedbacks ( `ID` BIGINT(11) NOT NULL AUTO_INCREMENT, `object_id` BIGINT(11) NOT NULL, `post_type` VARCHAR(45) NOT NULL, `positive` BIGINT(11) NOT NULL, `negative` BIGINT(11) NOT NULL, `updated` DATETIME NOT NULL, UNIQUE(`ID`) ) ENGINE = MyISAM DEFAULT CHARSET = utf8;とはいえ、これをそのまま渡してしまうと、ユーザー環境によってはテーブル接頭辞や文字コードが違うので、困ったことになってしまいます。あとは実用面の話なのですが、テーブル名を毎回ベタ書きしていると間違い探しで大変な思いをすることがあるので、変数に格納してしまいます。
class Anyway_Feedback{ var $table = "afb_feedbacks"; function sql(){ //wp-config.phpに書いてある文字コードを使用する $char = defined("DB_CHARSET") ? DB_CHARSET : "utf8"; return <<<EOS CREATE TABLE {$this->table} ( `ID` BIGINT(11) NOT NULL AUTO_INCREMENT, `object_id` BIGINT(11) NOT NULL, `post_type` VARCHAR(45) NOT NULL, `positive` BIGINT(11) NOT NULL, `negative` BIGINT(11) NOT NULL, `updated` DATETIME NOT NULL, UNIQUE(`ID`) ) ENGINE = MyISAM DEFAULT CHARSET = {$char} ; EOS; } function __construct(){ //初期化時にテーブル名を決定する global $wpdb; $this->db = $wpdb->prefix.$this->db; } }こうしておけば、$this->sql()とやるだけで正しいSQL文が取得できます。僕はヒアドキュメントが好きなのでこのように書きましたが、好きなように書いてください。あとはお作法として下記のような項目を守ればいいでしょう。
インストールされたバージョンをデータベースに残しておく
- データベースに残っているバージョン番号とプラグインのバージョン番号を比較して、古ければテーブルをアップデート
- そもそもテーブルが存在しなければアップデート
上記を実現するためには、クラスAnyway_Feedbackのコンストラクタにこんな処理を書き加え、なおかつバージョン変数を持たせます。
var $version = 0.3; function __construct(){ //オプションからafb_db_versionというデータを取得する。なければ0。 $this->db_version = get_option('afb_db_version', 0); }これで、アクチベーションフックを実行する段階で現在のバージョンとデータベースのバージョンを比較することができます。
それではアクチベーション関数をまともに動くようにしましょう。
function activate(){ global $wpdb; //データベースが存在するか確認 $is_db_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $this->table)); if($is_db_exists){ //データベースが最新かどうか確認 if($this->db_version >= $this->version){ //必要なければ関数を終了 return; } } //ここまで実行されているということはデータベース作成が必要 //必要なファイルを読み込み require_once ABSPATH."wp-admin/includes/upgrade.php"; //dbDeltaを実行 //データベースが作成されない場合はSQLにエラーがあるので、 //$wpdb->show_errors(); と書いて確認してください dbDelta($this->sql()); //データベースのバージョンを保存する update_option("afb_db_version", $this->version); }問題なければこれでデータベースが作成されるはずです。「作成はできたけど、更新もできるの?」と疑っている人はSQL文に新しいフィールドを追加して、$versionを増やしてみてください。停止 > 有効化とすると、テーブル構造が変わります。不思議ですね!
実はここらへんのことは全部Codexに書いてあります。
追記@2011-11-15
Twitterで教えていただいたのですが、上記の作法はWordPress3.1から使えなくなったようです。公式リポジトリからのアップグレードの場合に限るようですが。
https://twitter.com/#!/miya0001/status/136074413689933824
Codexを見ると、確かにそう書いてあります。日本語版を二年前に訳したのはぼくなんですが、追記があったみたいですね。
この場合、アクチベーションとディアクティベーションといった明確なフックを利用するのではなく、毎回チェックする必要があります。ただし、すべてのページでデータベースをチェックするのもアレなので、管理画面でのフック等を利用するといいのでしょうか。
//プラグイン読み込み完了後にフックを登録 add_action('plugins_loaded', 'afb_update'); //アップデート関数を管理画面においてのみ実行 function afb_activate(){ if(is_admin()){ //Do Stuff } }ディアクティベーションフックでテーブルは削除すべき?
さて、有効化のときになにかできるのであれば、無効化のときに何かをすることもできます。
「自分のプラグインが無効化されて悔しいので、ブログ所有者のデータベースも全部削除する」とかもできちゃうわけですが、基本的に無効化時にテーブル削除しない方がいいと思います。
というのも、WordPressは自動アップグレード時に下記の手順を取ります。
- すべてのプラグインを無効化
- WordPress本体をアップグレード
- 有効化されていたプラグインを有効化
なので、意図せずしてデータベースが削除されてしまうケースが考えられます(未検証)。また、「一回無効化したけどバージョンアップしてよさげだからもう一回使おう」なんて場合に、以前のデータがすべて消えていると悲しいので、データベースを削除する場合は、専用のページを設け、「データベースを削除する」というフォームを置いておくのがいいでしょう。
立つ鳥後を濁さずとは言いますが、phpMyAdminとかを見て「余計なデータが残っているなんて、なんて行儀の悪いプラグインだ!」とか言う人も削除用のフォームがあればガタガタ言わないと思います。
追記@2011-11-15
プラグインの情報を削除するタイミングはデアクティベーションではなく、プラグインのアンインストール(無効化してファイルを削除するとき)がいいみたいです。詳しくはWordPressで立つ鳥あとを濁さずなプラグインをつくるをご覧下さい。
CRUDを作成する
さて、データベースを持つプラグインができたので、データの基本的なCRUDを作ります。今回のケースでは以下の機能が必要です。
- データを取得する
- データを追加する
- すでに存在するデータのpositiveかnegativeフィールドの数値を加算する
- データを削除する
WordPressのデータベースインターフェースは$wpdbという変数です。これでほとんどのことはできてしまうので、詳しくはCodexを見てください。関数としてはget, add, update, deleteを作ればいいでしょう。
function get($object_id, $post_type = "post"){ global $wpdb; return $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table} WHERE object_id = %d AND post_type = %s", $object_id, $post_type)); }こんな感じで、関数内でグローバル変数の$wpdbを呼び出せば使えます。$wpdb->prepareはプリペアドステートメントなんですが、今回ハマったのは「MySQLのテーブル加算式がプリペアドステートメントで消されちゃう」という点ですね。ちょっとめんどくさくなってきたんで、githubで確認してください。
作った関数がちゃんと動くか確かめたい場合は、anyway-feedback.phpの一番下にこんな感じで関数を書いて、動くかどうか確かめましょう。
$afb->add(1, "post");これでphpMyAdminを確認し、テーブルにデータが追加されていたら動いているということです。他の関数も同様な方法で確認できますので、「データベースのCRUDは完璧だ!」と思えるまで検証しましょう。
ユーザーが入力できるようにする
今回のプラグインはユーザーのフィードバックを得るものなので、管理画面ではなく公開画面にフォームが必要です。また、Facebookはそうなんですが、いちいち画面遷移があるとユーザーにとって面倒だと思うので、Ajaxでリクエストを処理できるようにします。なにはともあれ、インターフェースを作らないことにはしょうがないので、プラグインのfunctions.phpにテンプレートタグを用意します。
function afb_display($class_name = null){ $class = "afb_container"; if($class_name){ if(is_array($class_name)){ $class .= " ". implode(" ", $class_name); }else{ $class .= " ". (string) $class_name; } } global $afb, $post; if($post) ?> <div class="<?php echo htmlspecialchars($class, ENT_QUOTES, "utf-8");?>"> <span class="message"><?php $afb->e("Is this article usefull?");?></span> <a class="good" href="<?php the_permalink(); ?>"><?php $afb->e("Useful"); ?></a> <a class="bad" href="<?php the_permalink(); ?>"><?php $afb->e("Useless"); ?></a> <input type="hidden" name="post_type" value="<?php echo get_post_type(); ?>" /> <input type="hidden" name="object_id" value="<?php the_ID(); ?>" /> <input type="hidden" name="nonce" value="<?php echo wp_create_nonce("anyway_feedback");?>" /> <span class="status"> <?php printf($afb->_("%d of %d people say this article is usefull."), afb_affirmative(false), afb_total(false));?> </span> </div> <?php }特に説明はありませんが、ループ内でしか使えません。まあ、はじめめなので、こんなものでしょう。あと、Ajaxなので、JSも読み込むようにします。Anyway_Feedbackクラスにこういう関数を用意しましょう。
function load_asset(){ //Load JS file. wp_enqueue_script( "anyway-feedback", plugin_dir_url(__FILE__)."assets/anyway-feedback-handler.js", array("jquery"), $this->version, true ); }wp_enqueue_scripは便利なので、積極的に使いましょう。wp_headにフックかけてechoしちゃだめですよ。ただし、これだけだと実行されないので、フックを使います。コンストラクタの中にこうやって書いてください。
add_action("wp_enqueue_scripts", array($this, "load_asset"));これでJSファイルが読み込まれます。ほんとうは必要なページだけでこのJSが読み込まれるのが一番いいのですが、そこまで考えているプラグインはあんまりないので、はじめだからこんなものかと妥協します。JSファイルはこんな感じで書きましょう。
jQuery(document).ready(function($){ $('.afb_container').find('a').click(function(e){ var endpoint = this.href; var data = { action: "anyway_feedback", object_id: $(this).nextAll("input[name=object_id]").val(), post_type: $(this).nextAll('input[name=post_type]').val(), nonce: $(this).nextAll("input[name=nonce]").val(), class_name: $(this).attr('class') }; $.post( endpoint, data, function(response){ if(response.success){ $('.afb_container a, .afb_container input').remove(); $('.afb_container .message').addClass('success').text(response.message); }else{ $('.afb_container a, .afb_container input').remove(); $('.afb_container .message').addClass('error').text(response.message); } } ); return false; }); });必要なデータをまるっと取得して、postでリクエストを行うという関数ですね。うまくいけば、ボタンを削除して、成功メッセージを出します。
Ajaxを処理する
まだこれでは完成ではありません。Ajaxリクエストを受け取って、successとmessageを持つJSONデータを返す必要があります。WordPressにはAjax用のエンドポイント(wp-admin/admin-ajax.php)が用意されていて、フックも登録できるようになっています。詳しくは5 tips for using Ajax in WordPressというエントリー(英語)を参照してください。このWordPerss Hardcoreというブログは大変深い記事を書いていて好きなのですが、半年に一回ぐらいしか更新されません。優秀な人は忙しいんですかね。
本当なら上記の方法でできるのですが、なんとミニコme!で利用しているプラグインTheme My Loginはwp-admin以下のリクエストにactionというキーを見つけると、強制的にリダイレクトするんですね。このせいで、Ajaxエンドポイントへのアクセスが強制的にねじ曲げられてしまいました。これに気づくのに1時間ぐらいかかりましたよ、ほんとに。プラグインとして便利かどうかと、APIにきちんと従っているかどうかは必ずしもイコールではないという好例ですね。
というわけで、しょうがないのでinitアクションにフックし、Ajaxリクエストとしてすべて該当しているのであれば、処理を行うようにします。
//コンストラクタ内に記述 add_action('init', array($this, "ajax")); //Ajax処理関数 function ajax(){ if(isset($_POST["action"]) && $_POST["action"] == "anyway_feedback"){ $nonce = isset($_REQUEST["nonce"]) ? $_REQUEST["nonce"] : "boo!"; $response = array( "success" => true, "message" => $this->_("Thank you for your feedback.") ); if(wp_verify_nonce($nonce, "anyway_feedback")){ //Feedback request is valid. switch($_POST["class_name"]){ case "good": $affirmative = true; break; case "bad": $affirmative = false; break; default: $response["success"] = false; $response["message"] = $this->_("Request is invalid."); break; } //If no error, try updating if($response["success"]){ if(!$this->update($_POST["object_id"], $_POST["post_type"], $affirmative)){ $this->add($_POST["object_id"], $_POST["post_type"], $affirmative); } } }else{ //Error. $response["success"] = false; $response["message"] = $this->_("Request is invalid."); } //Output result as JSON. header("Content-Type: application/json"); echo json_encode($response); //Don't forget exit. exit; } }ちなみに、「nonceって何?」という方はググってください…と思ったら、日本語情報がほとんどないですね。要するにCSRF(「ぼくはまちちゃん」 ――知られざるCSRF攻撃を参照のこと)を防ぐための仕組みだと思ってください。完璧には防げませんが、大体は防げます。
残りの作業
ここまででデータの登録はできるようになりました。ほんとうはすべてのステップを懇切丁寧に書こうと思ったのですが、ビール飲み始めたら「なんで僕は誰にも頼まれていないのにこんなことを書いているんだろう」とか「こんな専門的な記事を読む人がいったい日本に何人いるんだろう」とか「『あの花』の最終回はどうなったんだろう」とかが気になってきたので、あとはgithubに上げておいたソースで確認して下さい。説明していないけど実装した機能は以下になります。
- 役に立つと思われたコンテンツを投稿タイプ別にリストで表示するウィジェット
- 国際化対応
あと、あったらいいねという機能は以下になります。
- デフォルトCSSまたはユーザーのカスタムCSS自動読み込み機能、さらにCSSをオフにもできる
- テンプレートタグを書かなくても、自動でコントローラが出力される機能
- コメントループへのフォーム追加(このコメントは役に立ちましたか?)
- 出力系関数もろもろ
- 管理画面への統計情報出力(よくある質問の中で役に立ってるのはこれです、みたいなやつ。グラフもあると完璧)
WordPressのコーデックスで参照しておいた方がいいページは以下です。
- プラグイン用にテーブルを作成する
- 管理メニューの追加(プラグイン用の管理画面の作成)
- wpdbクラス(データベースインターフェース)の説明
- プラグインのAjax(Snoopyクラスとか使ってるのでちょっと古い)
- ウィジェットAPI
というわけで、大変尻すぼみになって申し訳ないですが、これで終わりです。プラグイン開発は4時間だったので、1時間以上は執筆にかけたくないですしね。もっと教えろという場合はコメントなりはてブなりTwitterなりでメッセージをください。反響があるのであれば、プラグイン公開までの手順を記したいと思います。終わり。
[429] [429] Client error: `POST https://webservices.amazon.co.jp/paapi5/getitems` resulted in a `429 Too Many Requests` response: {"__type":"com.amazon.paapi5#TooManyRequestsException","Errors":[{"Code":"TooManyRequests","Message":"The request was de (truncated...)
追記@2011.7.4
その後、+8時間ほどの開発を経て、プラグインとして公開しました。けっこう時間かかりますね。
公式リポジトリにAnyway Feedbackという名前で登録してあります。本来ミニコme!用に作ったものなので、需要があるかどうかはよくわかりませんが、使ってみてください。管理画面から検索しても多分出てくると思います。
登録してみて面白いなーと思ったのは、特に告知したわけではないのに、2日ぐらいで20件以上ダウンロードされていたことですね。これが公式リポジトリの力か…。路地裏で商売してもダメという話に似ていますね。
ちなみに、ミニコme!の開発などでちょっと時間が取れていないのですが、自分で電子書籍を売るためのプラグインLiterally WordPressもリファクタリングしたのち、公式リポジトリに上げようと思っています。あれから半年、なにも電子書籍に限る必要はないかなと気づいたので、色んなファイル形式に対応できるようにしようと思っています。物書きに限らず、ミュージシャンや絵描きさんなどなど、期待しないで待っててください。
ちなみに、公式リポジトリへのアップロードにあたって参考にしたURLは以下です。
大体申請してから3日ぐらいでSubversionのリポジトリは提供されると思います。
要望などあれば、このページか、github.comのプロジェクトページまでお願いします。pull requestとか一回も受けたことないので、「俺ここのところ直しちゃったぜ」という方はリクエストしてみてください。