先日、拙作プラグインであるGianismをアップデートし、その新機能として「Google Analytics連携」をブログ記事で紹介したのですが、肝心の投稿表示方法について書いていなかったので、改めて記します。なるべく汎用的な内容になるよう心がけます。
前提知識
WordPressには幾つかのテーブルがあり、コンテンツを表示する場合、WordPressに用意されたAPIを利用すれば事足ります。
- wp_postsやwp_taxonomyなど
- 投稿本体の取得およびカテゴリーやタグなどのタクソノミーによる絞り込みはデフォルトの
WP_Query
クラスが司っています。大抵の人はnew WP_Query
やget_posts
に配列を渡す形で利用しています。 - wp_post_meta
- いわゆるカスタムフィールドが保存されるテーブル。このデータを利用した投稿の取得方法に
WP_Meta_Query
というAPIが用意されています。 - wp_users
- ユーザーデータが保存されているテーブル。このデータを利用したユーザーの取得には
WP_User_Query
というAPIが以下略。
こうした標準のデータ取得に関してはこれでいいのですが、たとえば「ランキングテーブル」というのを用意して、それに従って投稿を取得し、ランキングを作りたいという場合、どうすればよいのでしょうか。
今回はこんな感じのテーブル構造を想定します。
汎用的に使いたかったのでカラム名が具体的ではありませんが、すべての行に投稿ID(object_id)と測定日(calc_date)とPV(object_value)が保存されています。測定日で一定の範囲に絞り込めば週間ランキングも作れますし、月間ランキングや年間ランキングも作れます。
今回目指すものの完成品が破滅派のランキングページです。
まずはget_postsが動くようにしよう!
さて、今回は投稿を取得することが目的なので、WordPressの標準機能を拡張することを目指します。初期状態では次のようにすると「WordPress」という文字列を含む最新の投稿3件が取得できるでしょう。
$posts = get_posts(array( ‘post_type’ => ‘post', ‘post_status’ => ‘publish’, ’s’ => ‘WordPress’, ’posts_per_page’ => 3, ));
この関数は内部的にWP_Query
クラスのget_posts
メソッドで、WordPressで投稿を取得する場合、ほとんどがこのメソッドを通ります。そこで、こんな風にすると2014年のランキング上位3件を取得できることを目指します。
$posts = get_posts(array( ‘ranking’ => ‘yearly', ‘year’ => 2014, ’posts_per_page’ => 3, ));
query_varを追加
get_posts
に渡すことのできるpost_type
やs
などのキーは「クエリバー」と呼ばれており、デフォルトのものをCodexで確認できます。これらのクエリバーはフィルターで登録しておく必要がある(多分)ので、オリジナルのクエリバーであるrankingを追加します。
add_filter(‘query_vars’, function( array $vars ){ $vars[] = ‘ranking’; return $vars; });
これでクエリバーにrankingが追加されます。
フィルターをかける
WP_Query->get_posts
メソッドは渡された配列から発行すべきSQLを組み立てていきます。このSQLの組み立て過程には大量のフィルターが存在します。Codexだけだとちょっと流れがよくわからないので、ソースを読むのが一番いいのですが、それはそれで大変なので、ざっと説明しますと……
WordPressはORマッパーがないので、get_postsメソッドの中で一生懸命SQLを組み立てていくのですが、完成品は次の画像のようになります。
黄色で囲った部分はフィルターをかけることができる部分です。プラグインでよくあるのは、WHERE節にフィルターをかけて抽出条件を変更するとかですね。
で、これはとは別に、発行するクエリ全体や取得した結果全体に対してもフィルターをかけることができます。今回はめんどくさいのでクエリ全体を書き換えてしまいます。
このためのフィルターはposts_request
フィルターです。渡ってくる引数はSQLクエリとクエリ発行元となるWP_Query
クラスのインスタンスです。この引数はどのフィルターでも大体共通なので、WP_Query
が該当するかどうかをチェックして、該当する場合はクエリを書き換えるという流れになります。
今回はrankingがyearlyの場合はyearに指定された年のランキングを取るという風にします。wp_wpg_ga_rankingというのがランキング用テーブルですね。
add_filter(‘posts_request’, function($query, $wp_query){ global $wpdb; // getメソッドでクエリバーを取得してチェック if( ‘yearly’ == $wp_query->get(‘ranking') ){ // 指定された年数を取得。 // 指定されていない場合は0になるようにする。 $year = (int)$wp_query->get(‘year’); // オフセットと取得件数を設定 $per_page = intval($wp_query->get('posts_per_page') ?: get_option('posts_per_page')); $offset = ((max(1, $wp_query->get('paged')) - 1) * $per_page); // SQL文を組み立て $query = <<<SQL SELECT SQL_CALC_FOUND_ROWS p.*, ranking.pv FROM ( SELECT object_id, SUM(object_value) AS pv FROM wp_wpg_ga_ranking WHERE YEAR(calc_date) = {$year} GROUP BY object_id ) AS ranking INNER JOIN {$wpdb->posts} AS p ON p.ID = ranking.object_id WHERE p.post_status = 'publish' AND p.post_type = 'post' ORDER BY ranking.pv DESC, p.post_date DESC LIMIT {$offset}, {$per_page} SQL; } return $query; }, 10, 2);
おそらくこの部分で読者が97%ぐらい離脱したと思うのですが、何をやっているかというと……
- ランキング用テーブルを年数でフィルタリングして、投稿IDごとにPVを合算
- その結果に対し、投稿テーブルをJOINして、並び替え
- 取得する結果は標準とほぼ同じだが、期間内の総合PVをおまけにつける
ざっとこんな感じです。これであとはnew WP_Query(‘raking’ => ‘yearly’, ‘year’ => 2014)
とやれば普通にループを回せばまったく同じように動きます。
URLがranking/2014でオッケーにする
さて、これで普通に動くようにったのですが、わざわざpage-ranking-year.phpとか作りたくないですし、2014を2013に変えたら勝手に2013年のランキングを取得できるとありがたいですね。このためにはリライトルールを使う必要があります。
リライトルールの役割は「URLを解析してWP_Queryに渡せる配列に変換する」ことです。WordPressは大雑把に言ってしまうと、すべてのURLでリライトルールによる解析を行って投稿を取得する「だけ」のアプリケーションです。
したがって、URLをクエリバーに変換すればよいので、このためのフィルターを用意します。
add_filter('rewrite_rules_array', function($rules){ return array_merge(array( 'ranking/([0-9]{4})/page/([0-9]+)/?$' => 'index.php?ranking=yearly&year=$matches[1]&paged=$matches[2]', 'ranking/([0-9]{4})/?$' => 'index.php?ranking=yearly&year=$matches[1]’, ), $rules); });
リライトルールの正体は正規表現と置換結果の配列です。ポイントは以下2点です。
- $matchesという変数によって後方参照が有効になるので、年数などを渡すようにする。ただし、この仕様は「そんなのいわれないとわからないよ!」という鬼畜仕様ですね。
- pagedというクエリバーは他のすべてのページでも使われており、なおかつページネーションプラグインなどもこれをアテにしていることが多いので、なるべくURLの形式を/page/10とし、クエリバーをpagedにするよう心がけましょう。
さて、これでリライトルールが有効になると思いきやなりません。というのも、リライトルールは普段データベースに保存されていて、毎回生成してないんですね。つまり、コード上ではフィルターが追加されていても、実際に利用されるのはデータベースに残った古いリライトルールです。
このリライトルールが更新されるのは管理画面の「パーマリンク設定」に移動して「更新」を押した時です。なので、手っ取り早いのはリライトルールを追加してデプロイしたら、管理画面に移動してこの作業を行えばよいのです。そうもいかないという方はこんな方法をやってみてください。
リライトルールを確認(get_option(‘rewrite_rules’)
で取れる)し、自分のリライトルールが存在しなかったらflush_rewrite_rulesメソッドで強制的に更新させる。
あとは、この処理をどこでやるかですね。すべてのページでやる必要はないので、たとえば、管理者が管理画面にアクセスしたときだけこのチェックを働かせるとか。パフォーマンスと更新忘れによる地獄のデバッグをはかりにかけて、適当にやってください。
上記の処理がうまくいけば、ranking/2014でランキングが取得できるはずです。index.phpをちゃんと作っていれば、表示も問題ないでしょう。
まとめと補足
というわけで、ちょっと高度なWPのTipsでした。他のテーブル構造でも同じだと思います。
破滅派ランキングではさらにこんな機能が追加されています。
- 前回のPVと比較して順位が上がったのかどうか
- 月間ランキング、週間ランキング、デイリーランキング
- 標準にないクエリだとwp_titleが空文字を返すので、そこへのフィルター
また、ここまで書いただけでも大変メンドクサイということが伝わったと思います。それは僕も同じで、破滅派にある似たような機能(自分のコメント一覧、ユーザー検索ページ、レビューした投稿の一覧、etc)を色々作ったのですが、そこで毎回同じようなことやってられんわということで、オレオレフレームワークを作りました。気になる人は見てみてください。
WPametuというリポジトリがフレームワークで、そのクライアントがhametuhaテーマです。PHP名前空間に則ったフォルダに特定のクラスを継承したファイルをおいておくと自動で動くという、MVCを目指した衝撃的な内容になっています。なんでこんなものを作ったのか、自分でもよくわかりません。
[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...)