さて、前回まででダミーデータができたので、それぞれのユーザーが自分のフォロワー、フォローしている人を眺められるようにしましょう。ページの仕様はこんな感じにします。
- 該当するページは固定ページで実装する。
- ページ名がfollowersの場合は自分をフォローしている人、followingの場合はフォローしている人が出る。
- とりあえずフィルターフックで出す。
本当はですね、プラグインとしてきちんとリリースできるようになるまでは、いろいろやることがあるんですよ。課題としてはこんな感じです。
- followingやfollowersというようにページスラッグが固定でいいのだろうか? 自由に変えられた方がよいのでは(=管理画面にオプションを追加する必要が出てくる)
- テーマにテンプレートを追加することなく表示できるべきでは?(そうなんだけど、WordPressはブログ以外の情報を出すのが辛い)
とまあ、いろいろあるんですよ。でも、これ全部やってるとたぶん20回やっても終わんないので、とりあえず画面に出してみます。
それでは、こんな固定ページテンプレートを作成して、page-follower.php
と名付けましょう。ほぼtwenty-fifteenのpage.phpをコピペしただけです。
<?php /** * Template Name: フォロワー */ get_header(); ?> <div id="primary" class="content-area"> <main id="main" class="site-main" role="main"> <?php // Start the loop. while ( have_posts() ) : the_post(); // Include the page content template. get_template_part( 'content', 'page' ); // If comments are open or we have at least one comment, load up the comment template. if ( comments_open() || get_comments_number() ) : comments_template(); endif; // End the loop. endwhile; ?> </main><!-- .site-main --> </div><!-- .content-area --> <?php get_footer(); ?>
テンプレートを配置したら、上で紹介したfollowersとfollowingというスラッグの固定ページを2つ作り、テンプレートを指定しましょう。
これでですね、the_contentにあたる部分をフォロワーのリストで置き換えましょう。
まずはコントローラーから
Web開発でよく使われるフレームワーク(CakePHP, Ruby on Rails, Django, etc)では、MVCという考え方が採用されています。アプリケーションの動きをModel(データ)とView(見栄えというよりUI)およびController(コントローラー)にわける考え方です。
「カッココントローラーってなんだよ」と思われたかもしれませんが、実はぼくもよくわからないのです。ビューに反応してモデルを操作するのがコントローラーらしいのですが……。
今回はこんなのをコントローラーとしましょう。
- Models\Followersのメソッドを呼び出して、フォロワーを取得する
- それをthe_contentと置き換え、ループを回す
これだけです。ループの中で出力するリストはプラグイン内のテンプレートファイル(=View)としましょう。
まず、抽象クラスFreundschaft\Pattern\Controller
を作成し、そのサブクラスとしてFreundschaft\Controllers\FollowerList
を作成します。
<?php namespace Freundschaft\Pattern; use Freundschaft\Util\String; use Freundschaft\Util\Input; use Freundschaft\Util\ModelAccessor; /** * Class Controller * @package Freundschaft\Pattern * @property-read \Freundschaft\Util\String $string * @property-read Input $input * @property-read ModelAccessor $models */ class Controller extends Singleton { /** * Getter * * @param string $name * * @return Singleton|null */ public function __get($name){ switch( $name ){ case 'string': return String::getInstance(); break; case 'input': return Input::getInstance(); break; case 'models': return ModelAccessor::getInstance(); break; default: return null; break; } } }
まだ具体的なものを作っていないので、ゲッターを設定するぐらいしかやることがないですね。
続いて、サブクラス。保存場所間違えないでくださいね。不安な人はGithubのコミットログを見ながら。
<?php namespace Freundschaft\Controllers; use Freundschaft\Pattern\Controller; class FollowerList extends Controller { protected $followers = 'followers'; protected $following = 'following'; /** * Constructor * * @param array $settings */ protected function __construct( array $settings = array() ) { add_filter('the_content', array($this, 'theContent')); } /** * Filter 'the_content' * * @param string $content * * @return string */ public function theContent($content){ if( is_page($this->followers) ){ return $this->getFollowersList(); }elseif( is_page($this->following) ){ return $this->getFollowingList(); }else{ return $content; } } /** * Get followers list * * @return string */ protected function getFollowersList(){ return 'フォローされてるよ'; } /** * Get followings list * * @return string */ protected function getFollowingList(){ return 'フォローしてるよ'; } }
これで freundschaft.phpに初期化処理を書き込みましょう。
/** * プラグイン読み込み完了後に実行 */ add_action('plugins_loaded', function(){ // Ajaxコントローラーを初期化 Freundschaft\API\Ajax\Follow::getInstance(); // フォロワーリストを初期化 Freundschaft\Controllers\FollowerList::getInstance(); // WP-CLIのコマンドを登録 if( defined('WP_CLI') && WP_CLI ){ WP_CLI::add_command('follow', 'Freundschaft\\Commands\\Follow'); } });
早速ページを表示してみると、フォローしてるよ/されてるよと表示されたんじゃないでしょうか。
あとはgetFollowersListとgetFollowingListをどうやって実装するか、ですね。
コントローラーのやるべきことを考えよう
さて、2つのメソッドは大体似たような処理になります。
- モデルに問い合わせてデータを取得する
- データがなければ「ありません」と表示し、データがある場合はフォロワーの数だけループ処理を行う
ここで注意点としては、「フォロワーの数を全部表示することはパフォーマンス上よくない」ということですね。10,000件も一気に表示したら、サーバが落ちるかブラウザがハングアップするかのどちらかです。ページわけが必要。
となると、モデルにそういった処理が必要ですね。これをgetFollowersとgetFollowingsにしましょう。
モデルに処理を実装する
さて、以前作ったFreundschaft\Models\Followers
クラスにメソッドを追加していきます。
ページネーションはとりあえず、WordPressの管理画面で設定した「1ページに表示する投稿の数」を利用しましょう。これはよく使いそうなので、抽象クラスFreundschaft\Pattern\Model
に実装してしまいます。
public function __get( $name ){ switch( $name ){ case 'db': global $wpdb; return $wpdb; break; case 'table': if( is_multisite() && $this->unique_on_multisite){ return $this->db->base_prefix.$this->name; }else{ return $this->db->prefix.$this->name; } break; case 'string': return String::getInstance(); break; case 'posts_per_page': return (int) get_option('posts_per_page'); break; default: return null; break; } }
それではいよいよメソッドの実装です。受け取る引数はユーザーIDと「何ページ目か」ですね。DBの構造を思い出しながら、クエリを書いていきます。DBの各行は「follower_idがuser_idをcreatedにフォローした」ことを表現しているので、user_idが特定のIDをcraetedの新しい順に取得しましょう。
/** * Get followers of specified user * * @param int $user_id * @param int $paged * * @return array */ public function getFollowers($user_id, $paged = 1){ // Get offset $offset = (max(1, $paged) - 1) * $this->posts_per_page; // Make query $query = <<<SQL SELECT * FROM {$this->table} WHERE user_id = %d ORDER BY created DESC LIMIT %d, %d SQL; return $this->get_results($query, $user_id, $offset, $this->posts_per_page); }
あと、いま気づいたんですが、親クラスのオーバーロードが間違ってましたね。Pattern\Model
を修正してください。
public function __call($name, $arguments = array()){ switch( $name ){ case 'get_var': case 'get_results': case 'get_row': case 'get_col': case 'query': if( count($arguments) > 1 ){ return call_user_func_array(array($this->db, $name), array(call_user_func_array(array($this->db, 'prepare'), $arguments))); }else{ return call_user_func_array(array($this->db, $name), $arguments); } break; default: // Do nothing break; } }
それでは、これをvar_dump
してみましょう。今度はFollowerList->getFollowersListに戻って、こんな風にしてください。
protected function getFollowersList(){ $followers = $this->models->followers->getFollowers(get_current_user_id()); var_dump($followers); return 'フォローされてるよ'; }
どうでしょう。「あなたのフォロワー」を表示すると、ちゃんとでましたね。
しかし、これだとちょっと味気ないような気がします。誰が、とか、そういうのがわからないじゃないですか。
それに、WordPressにはWP_Userというクラスがあって、これが色々してくれるんですね。せっかくなので、戻ってくる配列を単なる数字の羅列じゃなくて、WP_Userの配列にしましょう。
このWP_User、引数の説明を見ると、こんな風に書いてあります。
@param int|string|stdClass|WP_User $id User’s ID, a WP_User object, or a user object from the DB.
wp-includes/capabilities.php
ユーザーID、WP_Userクラス、データベースから取得したオブジェクトのどれでもいいみたいですね。
したがって、MySQLのJOINというテーブル連結機能を使い、先ほどの結果と一緒にwp_usersテーブルのデータも取ってきてしまいます。で、戻り値を全部WP_Userに変換します。これをすべてモデルにやらせてしまえば、ファットコントローラー問題になりませんね。
public function getFollowers($user_id, $paged = 1){ // Get offset $offset = (max(1, $paged) - 1) * $this->posts_per_page; // Make query $query = <<<SQL SELECT u.*, f.created FROM {$this->table} as f INNER JOIN {$this->db->users} as u ON f.follower_id = u.ID WHERE f.user_id = %d ORDER BY f.created DESC LIMIT %d, %d SQL; $return = array(); $result = $this->get_results($query, $user_id, $offset, $this->posts_per_page); foreach( $result as $row ){ $return[] = new \WP_User($row); } return $return; }
変更点をまとめるとこんな感じです。
- wp_followersテーブルにwp_usersテーブルをJOIN。条件は”On wp_followers.follower_id = wp_users.ID”と書く
- SELECT で取得するカラムを変更。wp_usersの全部(*はすべてという意味)とwp_followersのcreated(フォローした日)
- エイリアス(wp_followers as f)を使用。別に使わなくてもいいのですが、何度もテーブル名を書くのが面倒なので。なお、同じテーブルを別の方法でJOINするときはエイリアスが必要になります。
これでページを表示してみると、かなり詳しく取れていることがわかるのではないでしょうか。
ビューを読み込む
続いて、ビューを読み込みます。今回は次のようなルールにしましょう。
- プラグインにviewフォルダを作成し、そこのテンプレートを読み込む。
- テーマ内に同名のファイルが存在する場合は、そちらを優先。親子テーマの場合は、親テーマのフォルダも見る
- 存在するユーザーの数だけビューファイルを読み込み、書くビューファイルにユーザーデータを渡す
この機能を実装するために必要なのは、取得したデータの回数だけビューファイルを読み込んで文字列として返す関数と、テンプレートを取得する関数ですね。
まず、ビューファイルの名称を決めましょう。今回はviews/follower.phpとします。テーマ内に同じ名前のファイルがあったら、それを優先します。では、こんな内容でviews/follower.phpを作成してください。
<?php var_dump($user->display_name); ?>
うまくいけば、ユーザーの表示名が出てくるというわけですね。
続いて、このファイルを発見して読み込む関数を作成します。Pattern\Controller
にこんなメソッドを実装しましょう。
/** * Get view file path * * @param string $file File name wihout extension * * @return string */ protected function getView($file){ $file .= '.php'; $default = dirname(dirname(dirname(__FILE__))).DIRECTORY_SEPARATOR.$file; // If default not found, throw error if( !file_exists($default) ){ throw new \RuntimeException(sprintf('テンプレート%sが存在しません。', $default), 404); } // Get child theme's file $css_path = get_stylesheet_directory().DIRECTORY_SEPARATOR.$file; if( file_exists($css_path) ){ return $css_path; } // Get parent theme's file $template_path = get_template_directory().DIRECTORY_SEPARATOR.$file; if( file_exists($template_path) ){ return $template_path; } // Not found. Return default. return $default; }
指定したビューファイルが存在すれば、それを返し、テーマにあればそれを優先ということですね。
プラグインにビューファイルが同梱されていないということは上あり得ないので、例外を投げてしまいます。ここはリリース前に潰しておくべきバグですね。
続いて、取得したデータの数だけビューを読み込む処理をPattern\Controllerに書きます。
/** * Render user list * * @param array $users * @param string $view * * @return string */ protected function renderUserList(array $users, $view){ $path = $this->getView($view); ob_start(); foreach( $users as $user ){ include $path; } $contents = ob_get_contents(); ob_end_clean(); return $contents; }
それでは、これを使ってサブクラスControllers\FollowerListに実装してみましょう。
/** * Get followers list * * @return string */ protected function getFollowersList(){ $followers = $this->models->followers->getFollowers(get_current_user_id()); if( empty($followers) ){ return '<p class="error">ユーザーは見つかりませんでした。</p>'; }else{ return $this->renderUserList($followers, 'follower'); } }
さて、これでうまく表示されるでしょうか?
できましたね。最高です。あとは、テンプレートをいい感じにしましょう。
<?php /** @var WP_User $user */ ?> <div class="follower-list"> <?php echo get_avatar($user->ID, 64); ?> <h3 class="follower-name"> <?php echo esc_html($user->display_name) ?> <small class="from"><?php echo human_time_diff(strtotime($user->created, current_time('timestamp'))) ?>前</small> </h3> <div class="description"> <?php echo wpautop(get_the_author_meta('description', $user->ID)) ?> </div> </div>
う、うーん、なんか見栄え良くないですね。CSSを読み込んで、それなりの見栄えを担保しましょう。読み込みはControllers\FollowerList
にやらせます。
/** * Constructor * * @param array $settings */ protected function __construct( array $settings = array() ) { add_filter('the_content', array($this, 'theContent')); add_action('wp_enqueue_scripts', array($this, 'enqueueAssets')); } // 中略 /** * Enqueue assets */ public function enqueueAssets(){ wp_enqueue_style('freundschaft-follower-list', plugin_dir_url((dirname(dirname(dirname(__FILE__))))).'assets/css/follower-list.css', array(), '1.0'); }
適当なユーザーのプロフィールも記入してみて、ある場合とない場合の見栄えも比較できるようにするといいですね。
@charset "UTF-8"; @import "compass"; .follower-list{ @include clearfix(); border-bottom: 1px solid #ddd; padding: 0.5em; &:first-child{ border-top: 1px solid #ddd; } .avatar{ float: left; margin-right: 16px; } h3.follower-name{ font-size: 1.2em; clear: none; margin: 0 0 0.5em 90px; small{ margin-left: 1em; font-weight: normal; color: #aaa; } } .description{ margin-left: 90px; p{ margin: 0.5em 0; } } }
まあ、こんなもんでしょう。本当はレスポンシブとかあるんですけどね。
残りのメソッドを実装
さて、これまでで「自分をフォローしている人の一覧」はできました。では、自分がフォローしている人の一覧はどうすればよいのでしょうか。
テンプレートの読み込みは書いてしまったので、モデルとコントローラーのメソッドを2つ追加するだけですね。
まずはモデルから。なるべく、モデルから書くようにしましょう。
// Models\Followers /** * Get followings list * * @return string */ protected function getFollowingList(){ $followers = $this->models->followers->getFollowings(get_current_user_id()); if( empty($followers) ){ return '<p class="error">ユーザーは見つかりませんでした。</p>'; }else{ return $this->renderUserList($followers, 'follower'); } }
つづいてコントローラー。
// Controllers\FollowerList /** * Get followings * * @param int $user_id * @param int $paged * * @return array */ public function getFollowings($user_id, $paged = 1){ // Get offset $offset = (max(1, $paged) - 1) * $this->posts_per_page; // Make query $query = <<<SQL SELECT u.*, f.created FROM {$this->table} as f INNER JOIN {$this->db->users} as u ON f.user_id = u.ID WHERE f.follower_id = %d ORDER BY f.created DESC LIMIT %d, %d SQL; $return = array(); $result = $this->get_results($query, $user_id, $offset, $this->posts_per_page); foreach( $result as $row ){ $return[] = new \WP_User($row); } return $return; }
どうでしょう。「ほとんどメソッドをコピペしただけじゃないか」というレベルでしたね。コピペする部分が多いということは、共通化できるということです。それは今後に譲りましょう。
まとめ
- MVCっていう概念があるんだって。
- コントローラーを作った
- 一覧が取得できた
今回は最新の10件しか取っていませんが、次回はtwitterのUIをパクって参考にして、スクロールすると読み込むようにしましょう。もう疲れたから終わり。
[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...)