fbpx

メニュー

WP_PostがFinal問題に一筋の光が

高橋文樹 高橋文樹

この投稿は 10年 前に公開されました。いまではもう無効になった内容を含んでいるかもしれないことをご了承ください。

なんでこんなことになってしまったのかはよくわからないのですが、僕のようにWordPressをWeb開発のフレームワークにしている場合、こんな風に考えることがあります。

  1. WordPressはなにもしなくてもサイトが出来上がって便利だなあ、管理画面もついてるし
  2. そうだ、投稿じゃなくて購入履歴のDBを追加しよう!
  3. 購入履歴一覧とか、購入履歴詳細とか、そういうのほしいなあ
  4. >>>>>突然のめんどくせ<<<<<

なんでめんどくさくなるかというと、WordPressは次のような処理を行っているからです。

  • WordPressで表示されるデータはすべて投稿のリストであり、それ以外はありえない、なぜならブログなので
  • 投稿のデータはWP_Postクラスの配列として格納される
  • だったらWP_Postを継承して、WP_Purchase_Log(購入履歴クラス)作ってなんかできないかな
  • final WP_Post
  • オワタ \(^o^)/

ここでWP_Postを拡張しうる仕組みが存在していれば、フレームワークとしてもうちょっと色々できて便利なのですが、そうはなっていません。

調べてみると、「なんでWP_Postがfinal(継承不可)やねん、継承できるようにしてやー」と言っている人がいます。

結果から言うと、ともに却下っぽい感じですね。Nacinというコアコミッターによると、「WP_PostはAPIじゃなくてセキュリティ用のクラスだからfinalなんだよ」とのことです。ふーん。妥当かどうかは知りませんが、理由あってのこと。じゃあ、しばらく変わらなそうですね。

で、なんとかならないかなと思ってやってみました。こんな感じです。

add_filter('query_vars', function($var){
    $var[] = 'subscriber';
    return $var;
});

まず、クエリバーにsubscriberという文字列を追加します。これによってWP_Queryはsubscriberというパラメータを受付けるようになります。

具体的に言うと、http://example.jp?subscriber=hogeを受付けるということですね。

これをリライトルールで拡張すると、http://example.jp/subscriber/hogeというURLで「hogeというsubscriberを表示するのね」となります。ここら辺、MVCフレームワークと同じルーティングですね。リライトルールのコードはメンドクサイので書いてません。Codexのカスタムクエリクエリ概要など参照してください。

続いて、本来なら取得するであろう投稿データではなく、ユーザーを取得してみます。WordPressはMVCではないので、SQLをいきなり書き換えます。

add_filter('posts_request', function( $request, \WP_Query &$wp_query){
    /** @var \wpdb $wpdb */
    global $wpdb;
    // WP_Queryにsubscriberが設定されている場合のみSQL変更
    if( $wp_query->get('subscriber') ){
        // 現在のページを取得。これはWordPressデフォルトの動作
        $page = max( (int)$wp_query->get('paged'), 1 );
        $per_page = get_option('posts_per_page’);
        // SQLを作成。SQL_CALC_FOUND_ROWSは必須。ないと無限ループになる。
        $query = <<<EOS
            SELECT SQL_CALC_FOUND_ROWS
                ID,
                'subscriber' AS post_type
            FROM {$wpdb->users}
            LIMIT %d, %d
EOS;
        $request = $wpdb->prepare($query, ($page - 1) * $per_page, $per_page);
    }
    return $request;
}, 10, 2);

本来なら投稿テーブルにデータを取りにいくSQLを書き換えて、ユーザーのIDを取得し、なおかつpost_typeという存在しない投稿タイプを割り当てているところですね。

これで http://example.jp?subscriber=hoge の場合、取得するデータが投稿ではなく、ユーザーデータに変わることになります。

つづいて、ループ内で参照するデータをカスタマイズしてみます。Subscriberというモデルクラスを参照可能にしましょう。ループ内でよく書くthe_postを呼び出すたびに、上記で取得したIDのSubscriberが割り当てられていきます。

global $subscriber;

add_action('the_post', function( \WP_Post &$post ){
    global $subscriber;
    $subscriber = new Subscriber($post->ID);
});

グローバル変数を使うことになってしまいました。無念です。しかし、これによってループ内で$subscriber->display_nameとやることを目指します。

/**
 * 購読者クラス
 *
 * @property-read string $display_name
 * @property-read string $avatar
 */
class Subscriber
{

    /**
     * ユーザーデータ
     *
     * @var WP_User
     */
    private $data = null;

    /**
     * 投稿タイプ
     *
     * @var string
     */
    public $post_type = 'subscriber';

    /**
     * コンストラクタ
     *
     * @param int $id
     */
    public function __construct($id){
        $this->data = new WP_User($id);
    }

    /**
     * ゲッター
     *
     * @param string $name
     * @return int|mixed|null
     */
    public function __get($name){
        switch($name){
            case 'avatar':
                return get_avatar($this->data->ID);
                break;
            case 'user_url':
                return false !== array_search($this->data->user_email, array('http://', ''))
                    ? $this->data->user_url
                    : get_author_posts_url($this->data->ID, $this->data->author_nicename);
                break;
            default:
                if( isset($this->data->{$name}) ){
                    return $this->data->{$name};
                }else{
                    return null;
                }
                break;
        }
    }
}

ちょっと長いですが、購読者のデータを表現したSubscriberクラスを定義してみました。

これであとはテンプレートを書きます。テンプレートについては、get_template_part(’loop’, get_post_type())とかでloop-[post_type].phpが読み込まれるような仕組みにすでになっているとしましょう。とうことは、変えるべきテンプレートはloop-subscriber.phpということになります。

<?php
/** @var Subscriber $subscriber */
global $subscriber;
?>
<li class="clearfix loop loop-post">
     <?= $subscriber->avatar; ?>
     <h4 class="post-title"><a href="<?= $user->user_url ?>"><?= esc_html($subscriber->display_name); ?></a></h4>
     <span class="date mono">
          <?= $subscriber->user_email ?>
     </span>
</li>

globalでグローバル変数を呼び出しているところがちょっとかっこわるいのですが、現時点ではこんぐらいですかねー。

load_templateという関数を見ると、requireの前にextract($wp_query->query_vars)としているので、$userをグローバル変数ではなく、$wp_queryの変数に格納すればいいのかもしれません。

では、どんな見映えになるのか見てみましょう。

試しにユーザー一覧を表示
試しにユーザー一覧を表示

どうでしょう、このブログの通常のアーカイブページの記事ボックスがユーザー情報に変わりました。普通のサービスならメールアドレス出すことはないとおもいますけどね。

ポイントとしてはページネーションがそのまま有効に機能しているところです。Subscriberクラスのゲッターを充実させていけばもっと色々できるでしょう。

詳しい人はWP_User_Queryを使えばいいんじゃないの?と思われるかもしれません。が、結局WordPressのルーティング(URLを解析してデータを取得する)とシームレスにつなぐためにはいまのところこういうアプローチもありではないかと。

でも、自前のルーティング作った方が楽かもしれませんね。うーん。悩ましいところです。

というわけで、WordPressでアプリケーションを開発している場合に役立つかもしれないという情報でした。

すべての投稿を見る

高橋文樹ニュースレター

高橋文樹が最近の活動報告、サイトでパブリックにできない情報などをお伝えするメーリングリストです。 滅多に送りませんので、ぜひご登録お願いいたします。 お得なダウンロードコンテンツなども計画中です。