WordPress 徹底解析(WordPressを拡張するプログラムの構成法 – 管理画面をつける)

作成した拡張の設定値を、ユーザが WordPress の管理画面から操作できるようにします。

WordPress には、既存の設定ページに項目を簡単に追加できる便利な Settings API があり、これを用いて地域IDを「投稿設定」画面から設定できるようにしてみましょう。

また、完成したプラグインを機能の明確化のためにリファクタリングを行い、最後の仕上げをしてみます。


Settings API による管理画面付与

前回までのお天気取得拡張の機能で取得するお天気の地域を変更するには、ソースコードを修正しコンストラクタ引数により地域IDを設定するしかありませんでした。

今回は、これをユーザが画面から設定できるようになる管理画面の追加を行ってみます。

このような簡単な項目追加の場合、既存の管理画面のページに設定フォームを追加する Settings API と呼ばれる管理画面用 API を用いると、実装も楽で、ユーザも分かりやすいでしょう。

使い方はいくつかの API 関数の呼び出しとフックの登録で実現できます。

登場するのは、

  • セクションの追加(add_settings_section)
  • 項目の追加(add_settings_field)
  • 項目登録(register_setting)

の3関数と、これらの呼び出しを実装したメソッドです。

この実装メソッドを admin_init アクションフックに登録するという手順を踏むことにより、管理画面への画面展開やオプション値の保存が自動的に行われるようになります。

実装メソッド名を setting とし、処理を抜粋した概念ソースコードは次のようになります。

class Otenki {
    /**
     * オプション値・キー.
     */
    const OPTION_KEY = 'otenki';

    /**
     * 管理画面の投稿設定に地域ID設定を追加.
     */
    function setting() {
        // 投稿設定画面
        $writing = 'writing';
        // 投稿設定にセクション追加
        add_settings_section(
            'otenki_setting_section'
            , 'お天気情報取得サービス'
            , array($this, 'echoSettingSection')
            , $writing);
        // セクションに地域ID指定テキストボックス追加
        add_settings_field(
            self::OPTION_KEY
            , '地域ID'
            , array($this, 'echoSettingField')
            , $writing
            , 'otenki_setting_section');
        // 設定に登録
        register_setting($writing, self::OPTION_KEY);
    }

    /**
     * セクション文字列出力.
     */
    function echoSettingSection() {
        echo '取得したい地域IDを指定してください。';
    }

    /**
     * 地域ID設定テキストボックス出力.
     */
    function echoSettingField() {
        echo '<input name="' . self::OPTION_KEY . '"'
        . ' id="' . self::OPTION_KEY . '"'
        . ' type="text"'
        . ' value="'
        . esc_attr(get_option(self::OPTION_KEY)) . '" />';
    }
}

$otenki = new Otenki();
add_action('admin_init', array($otenki, 'setting'));

管理画面「設定->投稿設定」に HTML が展開され結果は次のようになります。(赤部分)

otenki_admin

ソースコードを順に見ていきます。

まず、add_action により Otenki クラスの setting メソッドを登録します。

add_action('admin_init', array($otenki, 'setting'));

setting メソッド内で管理画面に対するセクション、項目の登録を行います。

function setting() {
    // 投稿設定画面
    $writing = 'writing';
    // 投稿設定にセクション追加
    add_settings_section(
        'otenki_setting_section'
        , 'お天気情報取得サービス'
        , array($this, 'echoSettingSection')
        , $writing);
    // セクションに地域ID指定テキストボックス追加
    add_settings_field(
        self::OPTION_KEY
        , '地域ID'
        , array($this, 'echoSettingField')
        , $writing
        , 'otenki_setting_section');
    // 設定に登録
    register_setting($writing, self::OPTION_KEY);
}

add_settings_section、add_settings_field 関数の引数はコールバック関数を持ち、それぞれの画面出力用に登録したメソッド(echoSettingSection、echoSettingField)から HTML を行います。

PHP 5.3 以降が使える環境であればメソッドを登録せず無名関数で次のようにも記述できます。利用できる環境であればこちらのほうが良いでしょう。(無名関数を用いない場合、コールバックに登録するメソッドは public にする必要があります)

// 投稿設定にセクション追加
add_settings_section(
    'otenki_setting_section'
    , 'お天気情報取得サービス'
    , function() {
        echo '取得したい地域IDを指定してください。';
    }
    , $writing);
add_settings_field(
    self::OPTION_KEY
    , '地域ID'
    , function() {
        echo '<input name="' . self::OPTION_KEY . '"'
        . ' id="' . self::OPTION_KEY . '"'
        . ' type="text"'
        . ' value="'
        . esc_attr(get_option(self::OPTION_KEY)) . '" />';
    }
    , $writing
    , 'otenki_setting_section');

echoSettingSection、echoSettingField、register_setting 各 API 関数で指定している ‘writing’ は投稿設定画面を指定する引数です。

指定できる設定ページには general(一般設定), reading(表示設定), writing(投稿設定), media(メディア)などが管理画面の「設定」ページと対応しており、今回は「投稿時」の地域IDの指定ということで、writing(投稿設定) を渡しています。

const OPTION_KEY で定義している ‘otenki’ がデータベースに格納される((wp_)options テーブル)のキーとなり、add_settings_field 関数の引数と、echoSettingField で出力している name、id 属性を一致させていることにも注目してください。この対応により、自前で管理画面の処理を実装することなく、画面が展開されデータベースに値が保存がされるようになります。

以上で設定が管理画面から保持できるようになりましたので、この地域IDを元に Otenki クラスを動作させるようにします。コンストラクタで一定となっている地域IDを、保持した設定値で上書きするように修正します。

/**
 * コンストラクタ・プラグインデフォルト値設定及び設定の取得.
 */
function __construct(
    $city = '016010', $css_class = 'otenki') {
    // LWWS URL 引数(地域別値)
    $this->city = $city;
    // 画像出力時の CSS クラス
    $this->css_class = $css_class;
    // 管理画面地域ID設定を取得し存在すれば上書き
    $value = get_option(self::OPTION_KEY);
    if(!$value) {
        $this->city = $value;
    }
}

get_option で設定値を取得し、あればその地域IDを用いる流れです。管理画面未設定時はデフォルトコンストラクタの引数で動作します。

この処理をもって、管理画面から地域IDを指定し、その値を使って拡張が動作するようになりました。

リファクタリングとシングルトン

ここまででついに目標とするお天気情報取得の処理が完成しました。

ここからは実装上、気になる部分の修正をしていきましょう。このような動きを変えずに、ソースのクリーンナップや拡張を行うことをリファクタリングと呼びます。

まずはクラスと WordPress のインターフェースに注目した全体の概念ソースを示します。

class Otenki {

    /**
     * コンストラクタ・プラグインデフォルト値設定及び設定の取得.
     */
    private function __construct(
        $city = '016010', $css_class = 'otenki') {
        // 「オプション値・キー」を使った設定値取得
        // 「地域ID」設定
    }

    /**
     * 記事公開フックで天気情報取得スケジュールを作成する.
     */
    function prepare($post_ID, $post) {
    }

    /**
     * LWWS から天気情報を取得し記事のカスタムフィールドに格納する.
     */
    function extract($post_ID) {
        // 「地域ID設定」取得
        // 「カスタムフィールド・メタキー」を使ったカスタムフィールド値登録
    }

    /**
     * カスタムフィールドに取得したお天気アイコン用 HTML をアクションフックで出力.
     */
    function output() {
        // 「カスタムフィールド・メタキー」を使ったカスタムフィールド値表示
    }

    /**
     * 管理画面の投稿設定に地域ID設定を追加.
     */
    function setting() {
        // 「オプション値・キー」を使った設定
    }
}

$otenki = new Otenki();

add_action('publish_post', array($otenki, 'prepare'), 10, 2);
add_action('the_otenki', array($otenki, 'output'));
add_action('extract_otenki', array ($this, 'extract'));
add_action('admin_init', array($this, 'setting'));

ソース下部の記述により、Otenki クラスは「$otenki = new Otenki()」からインスタンスが作成され、各 add_action により WordPress にフックが登録されます。

ここで気になるのが new Otenki() のインスタンス作成部分です。このクラスは地域IDの異なる複数のインスタンスが作成できるにもかかわらず、次の理由で正しい処理が行えません。

    • 「カスタムフィールド・メタキー」が一定で、他のインスタンスから値が上書きされてしまう。
    • 管理画面「オプション値・キー」が一定で、複数の地域IDを指定することが出来ない。
    • 管理画面のテキストボックスの name や id が一定で、複数の地域IDの入力欄が出力できない。

これらを対応するためには、固定値となっている部分に地域IDを連結付与するなど地域をユニークにする実装が考えられます。

ただしいずれにしてもインスタンスを増やすにはソースコードを修正し

$sapporo = new Otenki();
$kurume = new Otenki('400040');

などとするしか方法がありません。

つまりクラスを用いて全ての実装を行うには、Otenki クラスのインスタンスの生成を束ねるコントローラとなるもうひとつのクラスが必要になります。

$otenki = new OtenkiController();

コントローラクラスは、管理画面の設定やそのオプション値をもって複数の Otenki クラスの生成を行う、WordPress 内で唯一のインスタンスとなります。

もし仮にこのクラスが別なモジュールから new され複数のインスタンスが作成された場合は、管理画面やオプション値がおかしなことになるでしょう。 WordPress はひとつなので、対話を行うインスタンスもひとつといった意味合いです。

このように、ただひとつだけインスタンスの存在を許すクラスのことをシングルトンクラスと呼び、実装のテクニックにより唯一のインスタンスを確保することができます。

その実装の一部は次のようになります。インスタンスの取得を行うために new をしていないところに注目してください。

$otenki = OtenkiController::getInstance();

さて、このような複数のインスタンスを束ねるコントローラクラスの存在ですが、このお天気取得プラグインにはいささかオーバスペックです。複数の地域IDを扱える可能性を持たせてクラスをつくってあるものの、実にまだそういった要件がないためです。

しかしながら完成している Otenki クラスに、シングルトン性を確保しておくことには意味があることでしょう。他の開発者が別なモジュールから Otenki インスタンスを作成する可能性はほぼゼロではありますが、シングルトン実装を行っておくことで動作をソースコード上で明示化できます。

ではここからは Otenki クラスのシングルトン化を含むリファクタリングを行い、自分を含む開発者にソースコードを持って意思を伝達するテクニックを紹介していきます。

まずは、ここまで解説した「このクラスのインスタンスは唯一ひとつしか存在できない」というメッセージを表現するために、シングルトンパターンと呼ばれる実装を Otenki クラスに加えていきます。

最初に、Otenki コンストラクタを public から private に落とします。これで外部からのインスタンスの作成(new)が言語構文上不可能になります。

private function __construct(
    $city = '016010', $css_class = 'otenki')

次に、static メンバ変数とメソッドを使い、Ontenki クラスの new を一度だけしか行えないようにブロックし、インスタンスをひとつに保証します。まとめると次のようになります。

class Otenki {
    /**
     * シングルトンインスタンス格納.
     */
    static private $INSTANCE = null;

    /**
     * WordPress 拡張処理用 Otenki インスタンスの取得.
     * 
     * @return Otenki
     */
    static function getInstance() {
        if(self::$INSTANCE == null) {
            self::$INSTANCE = new Otenki();
        }
        return self::$INSTANCE;
    }

    /**
     * コンストラクタ・プラグインデフォルト値設定及び設定の取得.
     */
    private function __construct(
        $city = '016010', $css_class = 'otenki') {
        /* 省略 */
    }
}

この実装により Oteki インスタンスを得るには Otenki::getInstance() の記述が唯一の方法となり、返却されるインスタンスは常に同じ、ひとつに保証されます。

ちなみに、本来はシングルトンインスタンスを作成・返却する部分を、

/**
 * シングルトンインスタンス格納.
 */
static private $INSTANCE = new Otenki();

/**
 * WordPress 拡張処理用 Otenki インスタンスの取得.
 * 
 * @return Otenki
 */
static function getInstance() {
    return self::$INSTANCE;
}

と実装したいところなのですが、PHP の場合、

static private $INSTANCE = new Otenki();

とスタティックイニシャライザが使えないため、前述の方法で実装しています。(この実装はマルチスレッド環境では不具合を起こしますが、PHP は言語仕様がシングルスレッドモデルなので問題ないでしょう)

シングルトン化された Otenki をフックに登録します。

リファクタリング前のソースは以下のようになっていました。new を行いインスタンスの生成をしています。

$otenki = new Otenki();

add_action('publish_post'
    , array($otenki, 'prepare'), 10, 2);
add_action('extract_otenki'
    , array ($otenki, 'extract'), 10);
add_action('the_otenki'
    , array($otenki, 'output'));
add_action('admin_init'
    , array($otenki, 'setting'));

リファクタリング後のソースは、new は行わずインスタンスは getInstance() メソッドから取得することになりましたので、次のようになります。

add_action('publish_post'
    , array(Otenki::getInstance(), 'prepare'), 10, 2);
add_action('extract_otenki'
    , array (Otenki::getInstance(), 'extract'), 10);
add_action('the_otenki'
    , array(Otenki::getInstance(), 'output'));
add_action('admin_init'
    , array(Otenki::getInstance(), 'setting'));

シングルトン化によりインスタンスはひとつしか存在できないと明示でき、副次的効果としてグローバル変数 $otenki を削除することができました。これで完全に WordPress と Otenki は 1:1 の関係となりました。

シングルトン化が完了しましたので、次にこの拡張がどのように WordPress インターフェースを使っているかをソースコード上に表現してみます。

ユーザ(開発者)が意識する WordPree とのインターフェース(フック)は、記事公開(publish_post)と、テンプレートタグにお天気を表示する(the_otenki)のふたつだけで、その他はインスタンス内部で使われているものです。

逆に言えば、スケジュール実行(extract_otenki)と管理画面(admin_init)は Otenki クラスとは内部構造上切っても切れない関係です。

このことを表現するため、開発者が意識するフックをクラスの外側に、切り離せないフックをコンストラクタで定義します。

class Otenki {
    /**
     * コンストラクタ・プラグインデフォルト値設定及び設定の取得.
     */
    private function __construct(
        $city = '016010', $css_class = 'otenki') {
        // 投稿スケジュール用アクションフック登録
        add_action('extract_otenki'
            , array ($this, 'extract'));
        // 管理画面アクションフック登録
        add_action('admin_init'
            , array($this, 'setting'));
    }
}

add_action('publish_post'
    , array(Otenki::getInstance(), 'prepare'), 10, 2);
add_action('the_otenki'
    , array(Otenki::getInstance(), 'output'));

このように明示的に記述を分けておくことで、開発者は publish_post と the_otenki は単純に外部からコントロールしてもよいと考えられるようになります。

たとえば、条件によって記事公開時のお天気情報の取得を行いたくない場合は、開発者は安心して自身のモジュールに次のような実装ができます。(plugins_loaded は全てのプラグインロード後に呼ばれるアクションフック、remove_action はアクションフックから特定のフックを削除する関数)

add_action('plugins_loaded', function() {
    if(/*なんとか条件 */ false) return;
    remove_action('publish_post'
        , array(Otenki::getInstance(), 'prepare')
        , 10)
});

また上記のソースからも分かるとおり、シングルトン化したことにより他のモジュールから拡張を扱う場合でも「Otenki::getInstance()」メソッドが利用できるようになり、$otenki などのグローバル変数を介した処理よりも、どのモジュールの処理なのかを明確にすることができます。

まとめ

管理画面の追加とリファクタリングが完了しました。

今回のポイントは、

  • 既存の設定ページに、簡単なオプション値の項目を追加するには Setting API が利用できる。
  • WordPress とのインターフェースを取り持つクラスでひとつしかインスタンスが存在できない実装は、シングルトンパターンを利用するとソースコードがメッセージ性をもち、他のモジュールからも利用しやすい。
  • フックを利用する場合は、そのインターフェースをソースコード上に表現し、他の開発者が操作しやすいようにしよう。

でした。

次回はいよいよシーズン2最終回。 総まとめ+目次です。 お楽しみに。 🙂


目次

本連載はファイルがない状態からソースコードを実装していく方式を採っています。プログラムの構成法も含めて解説していますので、最初から読んでいくとより理解しやすいでしょう。

アクションフックのプラグインをつくる編
最初に、WordPress の拡張において重要なフックの考え方を、サンプルの動作のひとつである「記事公開時の処理追加」の実装を元に解説します。フックはこの後の画面出力、処理の非同期化や管理画面の付与でも使われていきます。
処理のクラス化
WordPress 拡張におけるモジュール設計として、PHP のクラスを用いた方法を紹介します。分かりやすく、他のモジュールとの競合を最小限に抑えられる手法です。
外部 API の呼び出し
LWWS などの REST API に対して WordPress からリクエストを送る解説です。また、ユーザによって設定が異なる値がある場合のモジュール構成も合わせて紹介します。
外部 API 呼び出しの非同期化
外的要因などで処理スピードの低下が懸念される場合の処理方法です。WP-Cron を用いたスケジュール化手法を解説しています。
テンプレートファイルからの情報出力
テンプレートファイルに処理を記述し出力する場合に、拡張モジュールと疎結合にする実装法を紹介します。アクションフックの活用例としても有効です。
管理画面をつける(この記事)
管理画面に設定欄を設けユーザが拡張の機能をコントロールできるようにする手法の解説です。また、本連載中のソースコードに最終的なリファクタリングをかけ、他の開発者にこの拡張の意味を表現するプログラミングテクニックも解説します。
まとめ
本連載の概要とまとめ、そして目次です。

WordPress 徹底解析(WordPressを拡張するプログラムの構成法 – テンプレートファイルからの情報出力)

テンプレートファイルに情報表示用の記述を追加し、カスタムフィールドに格納した値を、クラス化した処理から出力する手法を紹介します。 🙂


テンプレートファイル用のヘルパー関数

前回までで、記事公開時にカスタムフィールドへのお天気情報の登録と、そのテンプレートファイルへの出力を行うヘルパー関数の実装までができました。

今回は実際にカスタムフィールドの値をHTML出力する処理を記述してみます。

出力部分のみ抜き出した概念ソースは以下のようになります。

class Otenki {
    function output() {
        // お天気情報出力処理
    }
}

$otenki = new Otenki();

function the_otenki() {
    global $otenki;

    $otenki->output();
}

本処理を行った場合の、記事投稿時の処理シーケンスは以下のようになります。

otenki_seq4

テーマ・テンプレートファイルのお天気情報を出力したい部分で、the_otenki 関数の呼び出しを記述することで出力を行います。

お天気情報は記事ごとの情報になりますので、WordPress ループ内に実装します。

<?php while(have_posts()) : the_post(); ?>
    <?php the_title(); ?>
    <?php the_otenki(); ?>
    <?php the_content(); ?>
<?php endwhile; ?>

ヘルパー関数 the_otenki を介して実際に HTML 出力処理を行う output メソッドでは、カスタムフィールドを特定するために、ループ中の記事IDが必要になります。

このような場合、通常のテンプレートファイルと同様、拡張の処理中でも記事IDを取得する get_the_ID() テンプレート関数を使うことが可能です。 これはシーケンス図から見て取れるように、処理が WordPress ループ内で呼び出されるためです。

カスタムフィールド取得や正当性確認などの処理を入れると、シーケンスは次のようになるでしょう。

otenki_seq5

実際の output メソッドの実装は以下のようになります。

function output() {
    // ループ中の記事IDを取得
    $post_ID = get_the_ID();
    // カスタムフォールドから天気情報取得
    $otenki = get_post_meta($post_ID, Otenki::META_KEY, true);
    // カスタムフィールドが正しい形式でなければ何もしない
    if(!(isset($otenki[Otenki::LWWS_V1_JSON])
        && $this->isOtenki($otenki[Otenki::LWWS_V1_JSON]))) return;
    // お天気情報を元に HTML を構築して出力
    $today = $otenki[Otenki::LWWS_V1_JSON]['forecasts'][0];
    $image = $today['image'];
    $telop = $today['telop'];
    echo "<img src=\"${image['url']}\""
        . " width=\"${image['width']}\" height=\"${image['height']}\""
        . " alt =\"${telop}\""
        . " class=\"$this->css_class\" />\n";
}

get_the_ID 関数でループ中の記事IDを取得し、get_post_meta 関数でそのカスタムフィールド値を取得し正当性確認後、HTML に出力するのが大きな流れです。

出力処理のアクションフック化

今回のように WordPress のテンプレートファイルに対して出力用のテンプレートタグを増やす拡張では、テンプレートファイルと拡張がかかれたモジュールの関係を維持する必要があります。

具体的に言えば、この拡張をプラグインとして定義した場合に、the_oteki テンプレートタグがかかれたテーマとプラグインはセットで扱う必要があります。つまり、プラグインを無効化すると the_otenki 関数が定義されなくなるため、PHP がエラーとなりサイトがダウンしてしまいます。

(functions.php に実装された場合は、必ずテーマとセットで有効になりますのでこの問題は発生しません。)

この問題に対処するために拡張をプラグイン化した場合には、その存在をテンプレートファイルで確認する必要があります。

グローバル変数 $otenki の存在チェック。

<?php if(isset($otenki)) the_otenki() ?>

グローバル関数 the_otenki の存在チェック。

<?php
if(function_exists('the_otenki')) {
    the_otenki();
}
?>

以上のようにいくつか方法がありますが、いずれもテンプレートファイルの記述は冗長になります。存在確認の処理はテンプレートファイルでは意識したくないところです。

そこで、WordPress のアクションフックの仕組みを利用して確認処理を削除するテクニックを用います。

テンプレートファイルに記述する関数呼び出しを次のようにアクションフックの呼び出しに変更します。

<?php while(have_posts()) : the_post(); ?>
    <?php the_title(); ?>
    <?php do_action('the_otenki'); /* ←ここ */ ?>
    <?php the_content(); ?>
<?php endwhile; ?>

そして処理を行う output メソッドを、新設した the_otenki アクションに登録します。

$otenki = new Otenki();

add_action(
    'publish_post', array($otenki, 'prepare'), 10, 2);
add_action(
    'extract_otenki', array ($otenki, 'extract'), 10);
add_action(
    'the_otenki', array($otenki, 'output')); /* ←ここ */

アクションフックを実行する do_action 関数は、登録された関数がないと何も処理を行いません。

つまり output メソッドがない(=プラグインが有効化されていない)場合にもエラーを起こさずに素通してくれるようになるため、冗長な存在確認の実装を削除することができます。

このようにするとプラグイン無効時の考慮だけでなく、合わせて the_otenki が呼び出されたタイミングを他のからもフックできるようになります。

ここからさらにアクションフックを使ったプログラミング手法を用いて、たとえば、お天気を出したくないときの条件制御を、他の拡張から動的に行うなどという実装も行えることでしょう。

まとめ

地域ID指定の管理画面化を除き全実装が完了しました。

今回のポイントは、

  • 拡張内でも WordPress のテンプレートタグ・関数が用いることが出来る。その場合は WordPress の動作シーケンスを意識しよう。
  • 拡張を functions.php ではなくプラグインとして実装し、テーマと関連する関数がある場合は呼び出し元で存在チェックをしよう。
  • 存在チェックが冗長になる場合はアクションフックの仕組みを用い、新設したアクションを用いて処理関数を登録すると処理削除とともに拡張性も向上する。

でした。

WordPress のフック処理は強力です。なんとバージョン 1.2 から備わる由緒正しい機能で、WordPress のプログラム構成上の最大の特徴といっても過言ではありません。

フックを使いこなすことにより、横断的関心事に対して様々な処理を柔軟に拡張していくことができます。自身で拡張を実装する場合も、フックを積極的に使うことで WordPress 的なプログラミングができるようになるでしょう。

というわけで、次回は地域IDを指定する管理画面を付与してみます。 お楽しみに。 🙂

ソース

最後にここまでの、全てのソースコードを記載します。

コンストラクタで地域IDを指定する形ですが、プラグインとしては全機能が動く状態です。プラグインファイルもしくは functions.php にコピーすることで動作します。

今までの解説が全て実装されていますので、プログラムの流れを考えながら是非読んでみてください。

<?php
/******************************************************************************
 * Otenki Class
 * 
 * @author     hiromasa
 * @version    1.00
 *****************************************************************************/
class Otenki {

    /**
     * カスタムフィールド・メタキー.
     */
    const META_KEY = 'otenki';

    /**
     * APIバージョン情報(V1/JSON).
     */
    const LWWS_V1_JSON = 'LWWS_V1_JSON';

    /**
     * LWWS URL 引数(地域別値).
     * 
     * @see http://weather.livedoor.com/forecast/rss/primary_area.xml
     */
    var $city;

    /**
     * 画像出力時の CSS クラス.
     */
    var $css_class;

    /**
     * コンストラクタ・プラグインデフォルト値設定.
     */
    function __construct($city = '016010', $css_class = 'otenki') {
        // LWWS URL 引数(地域別値)
        $this->city = $city;
        // 画像出力時の CSS クラス
        $this->css_class = $css_class;
    }

    /**
     * 記事公開フックで天気情報取得スケジュールを作成する.
     * 
     * @param $post_ID
     * @param $post
     * @return none
     */
    function prepare($post_ID, $post) {
        // "今日"のポストの時だけ天気を取得
        if(date("Y/m/d", strtotime($post->post_date))
            != date("Y/m/d")) return;
        // 天気取得の非同期スケジュール作成
        wp_schedule_single_event(
            time()
            , 'extract_otenki'
            , array($post_ID));
    }

    /**
     * LWWS から天気情報を取得し記事のカスタムフィールドに格納する.
     * 
     * @param $post_ID
     * @return none
     */
    function extract($post_ID) {
        // LWWS(V1/JSON)より天気情報を取得
        $json = wp_remote_get(
            'http://weather.livedoor.com/forecast/webservice/json/v1?'
            . http_build_query(array('city' => $this->city)));
        // 取得できなかった場合は次回の公開に期待
        if(is_wp_error($json)) return;
        // リクエストから JSON データ取得し PHP 連想配列に変換
        $otenki = json_decode($json['body'], true);
        // 正常な JSON 形式ではなければ次回の公開に期待
        if(!$this->isOtenki($otenki)) return;
        // カスタムフィールドにバージョン情報付きで格納
        update_post_meta(
            $post_ID
            , Otenki::META_KEY
            , array(Otenki::LWWS_V1_JSON => $otenki));
    }

    /**
     * カスタムフィールドに取得したお天気アイコン用 HTML をアクションフックで出力.
     * 
     * @param none
     * @return none
     */
    function output() {
        // ループ中の記事IDを取得
        $post_ID = get_the_ID();
        // カスタムフォールドから天気情報取得
        $otenki = get_post_meta($post_ID, Otenki::META_KEY, true);
        // カスタムフィールドが正しい形式でなければ何もしない
        if(!(isset($otenki[Otenki::LWWS_V1_JSON])
            && $this->isOtenki($otenki[Otenki::LWWS_V1_JSON]))) return;
        // お天気情報を元に HTML を構築して出力
        $today = $otenki[Otenki::LWWS_V1_JSON]['forecasts'][0];
        $image = $today['image'];
        $telop = $today['telop'];
        echo "<img src=\"${image['url']}\""
            . " width=\"${image['width']}\" height=\"${image['height']}\""
            . " alt =\"${telop}\""
            . " class=\"$this->css_class\" />\n";
    }

    /**
     * お天気情報バリデーション.
     * 
     * @param unknown $otenki
     * @return boolean
     */
    function isOtenki($otenki) {
        if(!(is_array($otenki)
            && isset($otenki['forecasts'])
            && is_array($otenki['forecasts'])
            && isset($otenki['forecasts'][0]['telop'])
            && isset($otenki['forecasts'][0]['image']))) {
            return false;
        }
        return true;
    }
}

/******************************************************************************
 * Otenki - WordPress Interface Define
 *****************************************************************************/

$otenki = new Otenki();

add_action('publish_post', array($otenki, 'prepare'), 10, 2);
add_action('extract_otenki', array ($otenki, 'extract'), 10);
add_action('the_otenki', array($otenki, 'output'));
?>

目次

本連載はファイルがない状態からソースコードを実装していく方式を採っています。プログラムの構成法も含めて解説していますので、最初から読んでいくとより理解しやすいでしょう。

アクションフックのプラグインをつくる編
最初に、WordPress の拡張において重要なフックの考え方を、サンプルの動作のひとつである「記事公開時の処理追加」の実装を元に解説します。フックはこの後の画面出力、処理の非同期化や管理画面の付与でも使われていきます。
処理のクラス化
WordPress 拡張におけるモジュール設計として、PHP のクラスを用いた方法を紹介します。分かりやすく、他のモジュールとの競合を最小限に抑えられる手法です。
外部 API の呼び出し
LWWS などの REST API に対して WordPress からリクエストを送る解説です。また、ユーザによって設定が異なる値がある場合のモジュール構成も合わせて紹介します。
外部 API 呼び出しの非同期化
外的要因などで処理スピードの低下が懸念される場合の処理方法です。WP-Cron を用いたスケジュール化手法を解説しています。
テンプレートファイルからの情報出力(この記事)
テンプレートファイルに処理を記述し出力する場合に、拡張モジュールと疎結合にする実装法を紹介します。アクションフックの活用例としても有効です。
管理画面をつける
管理画面に設定欄を設けユーザが拡張の機能をコントロールできるようにする手法の解説です。また、本連載中のソースコードに最終的なリファクタリングをかけ、他の開発者にこの拡張の意味を表現するプログラミングテクニックも解説します。
まとめ
本連載の概要とまとめ、そして目次です。

WordPress 徹底解析(WordPressを拡張するプログラムの構成法 – 外部 API 呼び出しの非同期化)

外部APIの呼び出しなど時間がかかる処理で、投稿などの動作が遅くならないよう、WordPress に備わる WP-Cron という仕組みを利用して、実行を非同期化するテクニックを紹介します。 🙂


お天気API呼び出しの非同期化

前回は、アクションフックを用いて投稿時にお天気APIに問い合わせを行い、カスタムフィールドにお天気情報を格納する部分の実装を行いました。

主要部分の概念ソースコードは次のようになります。

class Otenki {
    function prepare($post_ID, $post) {
        // "今日"のポストの時だけ天気を取得判定
        // wp_remote_get 関数による http リクエスト
        // カスタムフィールドへの情報格納
    }
}

$otenki = new Otenki();
add_action(
    'publish_post', array($otenki, 'prepare'), 10, 2);

本処理を行った場合の、記事投稿時の処理シーケンスは以下のようになります。

otenki_seq3

今回注目するのは、上記黄色メモ部分「お天気APIの取得処理」と後続の処理の関係です。

APIの呼び出しには外的要因が多く、時間がかかったり予想外のデータが戻ってきたりする場合があります。自分の扱い知らぬところで動作するため、慎重な実装が必要です。

たとえば、ネットワークや対向 API サーバの都合で API 呼び出し処理に 60 秒かかってしまうとすると、後続がそれだけ遅れていきます。簡単なところでは投稿後、管理画面の再描画までにプラス 60 秒かかって可能性があるということです。これはちょっといただけません。

また、前回外部APIから取得した情報の処理にはしっかり正当性確認をしようという部分がありましたが、外的要因如何よっては PHP が Fatal Error で終了してしまうという不安もぬぐいきれません。 WordPress にはトランザクションロールバックの処理がありませんので、後続処理によってはデータが不整合してしまう可能性も出てきます。

もちろん、処理をなんとかひとつのトランザクションとして扱いたい場合はやむを得ないところなのですが、WordPress の場合主たる処理は記事の投稿です。今回のお天気のような付加情報は、また次回がんばろうという考え方ができます。

以上のような理由から、お天気APIの取得を記事投稿とは切り離した処理にしてしまおうというのが今回のお話です。このような実行の一部で、結果を待つこととなく処理を進めることを非同期処理と呼びます。

WP-Cron によるスケジュール

WordPress には決められた時間を元に、一定の処理を行う WP-Cron と呼ばれるスケジューラのが組み込まれています。たとえば、未来日に設定した投稿したが、その時間で公開されるのはこの機能の働きです。

また、WP-Cron で行う処理は、サイトや管理画面の表示とは別な流れで行われるようになっており、本筋の動作に影響がでません。これは本筋の処理から、バックグラウンドで動くスケジュール処理用の別な WordPress を起動する特殊な実装で動いているためで、次のように自分自身で WordPress に http リクエストを行い実行するようになっています。

/**
 * Send request to run cron through HTTP request that doesn't halt page loading.
 *
 * @since 2.1.0
 *
 * @return null Cron could not be spawned, because it is not needed to run.
 */
function spawn_cron( $gmt_time = 0 ) {

    /* 省略 */

    $cron_request = apply_filters( 'cron_request', array(
        'url' => site_url( 'wp-cron.php?doing_wp_cron=' . $doing_wp_cron ),
        'key' => $doing_wp_cron,
        'args' => array( 'timeout' => 0.01, 'blocking' => false, 'sslverify' => apply_filters( 'https_local_ssl_verify', true ) )
    ) );

    wp_remote_post( $cron_request['url'], $cron_request['args'] );

}

このような WP-Cron の特徴を生かすと、外部 API 取得処理のような時間のかかる可能性がある処理を非同期化することができます。WordPress 本体内でも更新 ping の送信や Akismet のスパムチェックなどで同様のテクニックが使われています。

では実際に WP-Cron を使ってお天気 API 取得処理の非同期化を行ってみます。

API がよくできていますので、WordPress でスケジュールをつくるのは難しくありません。ここではひとつだけスケジュールを作成する wp_schedule_single_event 関数を使います。

wp_schedule_single_event 関数は引数で指定した時刻以降に、指定したアクションフックを呼び出してくれます。

アクションフックは任意のものが指定できますので、お天気 API の取得を行うフックを “extract_otenki” と命名し、その処理がかかれた execute メソッドを登録(add_action)してあげます。

前回つくった prepare の取得処理部分だけが execute メソッドに移り、代わりに wp_schedule_single_event でスケジュール登録する形になります。

最初の概念ソースコードとの差分は次のようになります。

publish_post アクションフックだけで処理していた最初のソース。

class Otenki {
    function prepare($post_ID, $post) {
        // "今日"のポストの時だけ天気を取得判定
        // wp_remote_get 関数による http リクエスト
        // カスタムフィールドへの情報格納
    }
}

$otenki = new Otenki();
add_action(
    'publish_post', array($otenki, 'prepare'), 10, 2);

wp_schedule_single_event で処理を非同期化したソース。

class Otenki {
    function prepare($post_ID, $post) {
        // "今日"のポストの時だけ天気を取得判定
        // 天気取得の非同期スケジュール作成
        wp_schedule_single_event(
            time()
            , 'extract_otenki'
            , array($post_ID));
    }

    function extract($post_ID) {
        // wp_remote_get 関数による http リクエスト
        // カスタムフィールドへの情報格納
    }
}

$otenki = new Otenki();

add_action('publish_post'
    , array($otenki, 'prepare'), 10, 2);
add_action('extract_otenki'
    , array ($otenki, 'extract'), 10);

prepare メソッドと extract メソッドで処理対象となる記事IDが、wp_schedule_single_event の引数を介して受け渡しできている部分にも注目してください。

ここまできてようやく、publish_post のアクションフックに登録している関数名が prepare(準備)になっていた謎が解明されたのでした。 🙂

まとめ

以上で、お天気 API 取得処理の非同期化ができました。

今回のポイントは、

  • 外部 API 呼び出しなど外的要因で速度の低下が見込まれる場合は非同期化しよう。
  • WP-Cron にスケジュールされた処理は、サイトの表示処理とは別な WordPress が起動して行われるので非同期化される。
  • wp_schedule_single_event は WP-Cron にスケジュールを行う関数で、実行時刻の登録とアクションフック名と、アクションフックへの引数を登録することが出来る

でした。

というわけで、また次回。 🙂


目次

本連載はファイルがない状態からソースコードを実装していく方式を採っています。プログラムの構成法も含めて解説していますので、最初から読んでいくとより理解しやすいでしょう。

アクションフックのプラグインをつくる編
最初に、WordPress の拡張において重要なフックの考え方を、サンプルの動作のひとつである「記事公開時の処理追加」の実装を元に解説します。フックはこの後の画面出力、処理の非同期化や管理画面の付与でも使われていきます。
処理のクラス化
WordPress 拡張におけるモジュール設計として、PHP のクラスを用いた方法を紹介します。分かりやすく、他のモジュールとの競合を最小限に抑えられる手法です。
外部 API の呼び出し
LWWS などの REST API に対して WordPress からリクエストを送る解説です。また、ユーザによって設定が異なる値がある場合のモジュール構成も合わせて紹介します。
外部 API 呼び出しの非同期化(この記事)
外的要因などで処理スピードの低下が懸念される場合の処理方法です。WP-Cron を用いたスケジュール化手法を解説しています。
テンプレートファイルからの情報出力
テンプレートファイルに処理を記述し出力する場合に、拡張モジュールと疎結合にする実装法を紹介します。アクションフックの活用例としても有効です。
管理画面をつける
管理画面に設定欄を設けユーザが拡張の機能をコントロールできるようにする手法の解説です。また、本連載中のソースコードに最終的なリファクタリングをかけ、他の開発者にこの拡張の意味を表現するプログラミングテクニックも解説します。
まとめ
本連載の概要とまとめ、そして目次です。