fbpx

メニュー

WP REST APIを利用したNodeJSのWebアプリを死ぬ思いで作る

高橋文樹 高橋文樹

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

この記事はWordPress Advent Calendar 2015の5日目担当です。このアドベントカレンダーで最長の記事になるよう頑張ります。Webフォントのレンダリングがぶっ壊れるまで、僕は、書くのを、やめないっ!

目次

  1. Calypsoの公開が意味するもの
  2. WP REST APIの基本
  3. OAuthプロバイダーになる
  4. クライアントサイトをNodeJSで作成しログイン
  5. カスタムエンドポイントを作成する
  6. まとめ

Calypsoの公開が意味するもの

さて、先日WordPress.comCalypsoというアプリケーションを公開しました。コードはGithubに公開されているのですが、NodeJSで内部サーバを立ててそれをアプリとしてラップしているような構造です。UIはReactで作成されており、バックエンドの処理はWordPress.comのREST APIと通信することで実現しています。

Calypsoって要はこういうことだろ?
Calypsoって要はこういうことだろ?

一応オープンソースにはなっていますが、個人的には、WordPress.orgのユーザーにとって以下の点であまり恩恵がないのかなと思っています。

  • APIのエンドポイントがWordPress.comのものであり、これはWordPressで公式に採用されると噂されているWP REST APIとは異なる。
  • Reactを採用したことでWordPress.orgのコードベースとはかけ離れたものとなり、おそらく移植は困難。

なんですかね、ラノベのタイトルで言うと、「幼なじみがいつのまにかリア充になってて俺の全然知らない奴らと遊んでるんだが……」みたいな感じでしょうか。

もちろん、コミュニティ的な決断として、サーバサイドの処理(とくに管理画面)を頑張ってWP REST APIにして、フロントエンドをReactにしてとか、そういう展開はありえますが(逆になかった場合どうなるんでしょう)、たとえばWP Image EditorはBackbone.jsでゴリゴリ書かれているので、その資産どうすんのとか、フロントエンドにまさかのMarionette.js採用すんのとか、色々悩ましい点があるでしょう。こういったことを本当に乗り越えられるのか、個人的には興味津々です。

Automattic将来、Calypso がすべての WordPress と共に動くアプリになれるよう、来年以降の WP REST API プロジェクト進行状況によって変化をとげていく予定だそうです(引用元:Calypso: WordPress.com の新しい方向性)。でも公開まで一年半もいじってたソースをそんな簡単に別プロジェクトにマージできるもんなんでしょうか。Xoopsみたいにならないといいのですが……。ちなみに、NodeのMongoDBライブラリmongooseをホスティングしてるの、Automatticなんですね。知らなかった。

とまあ、いきなり懐疑的な視線を向けて性格の悪さを露呈してしまいましたが……来年のWordPressのトレンドはこれ!

  • 今後はREST APIを充実させ、サーバサイドの処理を疎結合にしていく。
  • フロントエンドはSPAっぽくしていき、非同期処理で軽快に動作。

あと、WordPressをアプリケーションプラットフォームとして使っている人は「いまの管理画面はいらない」という決断を早晩下すはめになると思います。だって、FacebookもTwitterも管理画面ないですよね? いくらなんでもCalypsoみたいに管理画面をまるごと再設計する必要はないと思いますが、たとえば、レシピ投稿サイトを作っているとしたら以下の機能をユーザーに提供する必要があります。

  • アカウント管理(プロフィール編集、ログイン情報の編集などなど)
  • レシピ投稿・編集画面

これらの機能をWordPressの管理画面に頼らず実現したくなるでしょう。では、どうしたらいいのでしょうか?

ということで、ここまでで前段終了、いよいよ本題に入ります。

WP REST APIの基本

WP REST APIについて理解する前に、そもそもRESTってなんやねんというお話。Wikipediaによると……

Representational State Transfer(REST) は、ウェブのような分散ハイパーメディアシステムのためのソフトウェアアーキテクチャのスタイルのひとつである。この語は2000年に、HTTPプロトコル規格の主要著者の一人であるRoy Fieldingが、ウェブについて書いた博士論文で初めて現れ、ネットワーキングコミュニティの中ですぐに広く使われることになった。
REST Wikipedia

なるほどわからん。

実際に利用されているケースを見ると、だいたい次のような感じです。

ステートレス

Webの通信はそもそもステートレスなのであまり意識することはないですが、RESTではすべてのリクエストがその処理を行うために必要な情報を必ず含んでいます。要するに、ログインしているならその情報も全部持ってろよということですね。

アドレスでなにをしようとするかを判別

URIがそのまま処理を意味します。たとえば破滅派ではフォローやアンフォローをするための機能をWP REST APIで実現しているのですが、そのエンドポイントは次の通り。

/wp-json/hametuha/v1/doujin/follow/3/ ユーザーID3(高橋文樹)をフォローする

また、RESTでは通常GET, POST, PUT, DELETEというメソッドがあるのが普通で、上のURIにPOSTするとフォロー開始、DELETEだとフォロー解除という感じです。PUTはPOSTと何が違うのかよくわからないのですが、この4つがあるのが普通ですね。

レスポンスはJSON

昔はXMLでした。TwitterのREST APIとかはフォーマット選べますが、普通はJSONで返ってきます。

OAuthとセット

OAuthというのはAPI認証の共通規格です。上述の通り、RESTはステートレスなので、毎回認証が必要なのですが、そこでパスワード投げ続けるのも不健全なので、OAuthを使います。ちょっと前はOpenIDとかありましたが、今はOAuthが普通ですね。OAuthについては後述します。

 

それでは、実際にお試ししてみましょう。

まず、お手元のWordPressにWordPress REST APIプラグインをインストールします。バージョン2をインストールしてください。

WordPress 4.4からコアにマージされたので、最新バージョンをお使いの方はこのプラグインをインストールする必要はありません。(さらに追記:一部しかマージされていないので、やっぱりインストールは必要!

これを有効化したら、 http://example.com/wp-json/ にアクセスすると、ずどどどーっとエンドポイントが表示されます。これはいうなればマニュアルみたいなもんですね。

エンドポイントのリストが表示されます。
エンドポイントのリストが表示されます。

さて、こちらにアクセスしてみましょう。認証なしでも表示できるエンドポイントはあるのですが(ex. 投稿の一覧)、そんなものを表示してもFeed APIとの違いがよくわからないので、認証が必要なエンドポイントをわざわざ表示しましょう。

WP REST APIのドキュメントによると、APIでの認証には2通りあります。

Cookie認証

WordPressの通常のログインと同じく、Cookieに保存されている情報を頼りにログイン判定を行います。ただし、nonceが設定されていないと表示はされません。

OAuth認証

OAuth認証の具体的な方法は後で説明しますが、スマホアプリや別のWebサイトから認証する場合はこちらを使います。

 

今回はCookie認証で情報を取得してみましょう。このようなコードをお手元のWordPresssのfunctions.phpに書いてください。Javascriptを読み込むコードです。wp_enqueue_scriptを使って、WP REST APIが読み込む認証用のJSへの依存を解決します。

add_action('wp_enqueue_scripts', function(){
    wp_enqueue_script('my-api', get_stylesheet_directory_uri().'/api.js', ['jquery', 'wp-api'], '1.0', true);
});

で、読み込むJSはapi.jsというものを書きましょう。とりあえず、wp-apiが渡してくれる情報を見てみましょう。

(function ($) {
    'use strict';
    console.log(WP_API_Settings);
})(jQuery);

なるほど、エンドポイントのルートとnonceが設定されるわけですね。

console.logすると、rootとnonceを持つオブジェクトらしい
console.logすると、rootとnonceを持つオブジェクトらしい

では、これで情報を取得してみます。ドキュメントによると/users/me/で自分の情報が取れるはずなので、取得してみます。ポイントは普通のAjaxリクエストにX-WP-NONCEヘッダーを追加して、CSRF脆弱性を防ぐことですね。

(function ($) {
    'use strict';
    $.ajax({
        url       : WP_API_Settings.root + 'wp/v2/users/me',
        method    : 'GET',
        beforeSend: function (xhr) {
            xhr.setRequestHeader('X-WP-Nonce', WP_API_Settings.nonce);
        }
    }).done(function (response) {
        console.log(response);
    });
})(jQuery);

どうでしょう、これでユーザー情報が取得できました。

僕のユーザー情報が取得できました。
僕のユーザー情報が取得できました。

他にも投稿を作成したり、色々できます。が、いまは先に進みましょう。

OAuthプロバイダーになる

さて、認証方法には2つあると上で書きましたが、もう一つのOAuth認証を可能にするためには、WordPress自体がOAuthプロバイダーでなくてはなりません。そのためのプラグインがあるのですが、なんとCLIからしかコンシューマーキー取得できないので、僕がフォークして新しいのを作りました。

HamAPI Provider

こちらをダウンロードして、有効化すると、ユーザーメニューの下にアプリケーション登録リンクができます。

まさかのユーザーメニュー
まさかのユーザーメニュー

こちらで新規アプリケーションを作成すると、コンシューマーキーとコンシューマーシークレットが取得できます。

アプリを作成すると認証情報が取得できる
アプリを作成すると認証情報が取得できる

http://example.com/wp-json/ でエンドポイントを確認すると、/oauth1/* という認証用エンドポイントが増えているのがわかります。

新たに増えた認証用エンドポイント
新たに増えた認証用エンドポイント

あとはコールバックURLのホワイトリスト編集という作業が地味に残っています。プラグイン側で実装しようと思ったのですが、ちょっと忙しいので、テーマ内のfunctions.phpに記載してもらってもよろしいでしょうか。

今回は実験なので、外部からのコールバックリクエストをすべて許可という男前な仕様にします。実際はちゃんと判定してくださいね。※ここって外部サイトかどうかの判定かもしれないので、公開サーバだと案外問題なかったりするかもしれません。

/**
 * Allowed sites for API callback
 */
add_filter( 'http_request_host_is_external', function ( $allow, $host, $url ) {
    // 本当はここで$hostが安全かを判定するはず
    // 今回は特例で全部スルー
    return true;
}, 10, 3 );

さて、これでOAuthプロバイダーになることができたのですが……肝心のクライアントが存在しないので、いまはまだ何もできません。それでは実際にNodeJSのアプリケーションを動かしてみましょう……さあ、地獄の幕開けだ!

クライアントサイトをNodeJSで作成しログイン

一応、WordPress Advent Calendarなので、いきなりNode前提というわけにも行かないんですが、環境をどうしましょうか……

とりあえず、破滅派で公開しているサイトのVagrant環境を適当に削ったものを用意しておきましたので、こちらからダウンロードしてください。ダウンロードしたら、README.mdにしたがって、諸々インストールします。

で、Vagrantfileの冒頭に設定する部分がありますので、そちらに必要な情報を記載してください。

HAMETUHA_CONFIG = {
  :host_name       => 'local.hametuha.pics', # クライアントサイトのホスト名 変えなくて大丈夫。
  :wp_host         => 'takahashifumiki.info', # ホストとなるWordPressのドメイン。 ローカルにあるWordPress。
  :wp_url          => 'https://takahashifumiki.info', # ホストとなるWordPressのURL。同上。
  :consumer_key    => 'ikjTnbUBsTsC', # さっき作成したコンシューマーキー
  :consumer_secret => 'dihm2jxs0WVr70OFS6jZzTgEBtgWoevcvE50ao7zHNVk6Bx0', # さっき作成したコンシューマーシークレット
}

準備が済んだら次のコマンドをターミナルで発行。「ターミナルとは?」という方はこちらの電子書籍を購入してください。

berks vendor ./cookbooks
vagrant up

どうです? 動きました? 最後エラーが出るかもしれませんが、完了するはずです。

mongodbのところでコケる
mongodbのところでコケる

動かない人はお疲れ様でした。またお会いしましょう。こんなに長く付きあわせて申し訳ありませんでした。はてぶをつけてお帰りください。

無事動き出した人も少し時間がかかるので、以下のコラムをお読みください。

さて、閑話休題。Vagrantが立ち上がったら、必要なライブラリのインストールを行いましょう。ターミナルで次のコマンドを打ってください。

# Vagrantにログイン
vagrant ssh
# アプリルートに移動
cd /var/www/app
# ライブラリのインストール
npm install
bower install
# CSSなどを生成
gulp build
# アプリを起動
# 終了するにはCtrl + C
npm start

/var/www/appがアプリケーションのルートですね。インストールを終えたところで、さっそくコードを確認しましょう。いくらなんでもこのエントリーだけでExpressの使用方法を教えることはできないので、ざっと説明します。

このソースについて

このソースは はめぴくっ! という電子書籍の表紙画像作成サービスのソースを削ったものです。無駄にスタイルとかあたってますが、そこら辺はご愛嬌ということで。

電子書籍の表紙を作れるサービスであり、出合い系サイトではありません。
電子書籍の表紙を作れるサービスであり、出合い系サイトではありません。

Vagrantが立ち上がったら、app/configフォルダにdefault.jsonというファイルがあるはずです。こちらの内容が先ほどVagrantfileに書いた内容になっているでしょうか。なっていれば、これで設定は完了です。以降、example.jpといったら、あなたがローカルにインストールしているお手元のWordPressのことを指します。

あとはソースを読むための基礎知識を説明。

NodeJSの基本

NodeJSにはCommonJSという仕組みがあります。

// hoge.js
module.exports.fuga = function(hoge){
    return hoge + 'ふが';
};

// main.js
var hoge = require('./hoge');
hoge.fuga('ほげ');
// ほげふが

module.exportsとかそういう構文でモジュールを定義しつつ、それをrequireで呼び出します。gulpを自分で書いたことがある人は、requireと書いているのでよく知っているでしょう。

Javascriptはすっぴんのままだとなにもできないので、ファイル読み込みやらパスの取得やらディレクトリの作成やらでもrequireでいろんなモジュールを呼び出す必要があります。

Expressの基本

まず、このリポジトリはExpressというNodeJSのフレームワークをベースに作られています。Pythonのtornadoとかも似たような感じですが、こんな構文です。

app.get('/foo', function(req, res, err){
     // http://example/foo で反応する
});

// 関数を複数アサインすることもできます。
// その場合は左から順に実行されます。
app.get('/var', func1, func2);

// レスポンスメソッドのrenderで画面を描画
// テンプレートはviews/about.jadeを使用
app.get('/about', function(req, res, err){
     res.render('about', {
          title: 'Expressとは?' // ここはテンプレートに変数として渡せる
     });
});

app.postならPOSTメソッドの時だけ反応とか、そんな感じです。勘のいい方はお気づきだと思いますが、わざわざプラグインなど利用しなくてもRESTフルな作りになっているんですね。これはなにもExpressに限ったことではなく、昨今のWebフレームワークはだいたいそうなってます。WordPressはかなりレガシーです。高校進学したら俺だけ私服ダサかったみたいな感じでいたたまれないですね。

こうしたことを踏まえて、メインファイルであるapp.jsのソースを読んでみましょう。全部読むのは大変なので、前半はざっと飛ばします。

16行目まで
利用するライブラリの読み込み。requireを連発しています。
36行目まで
初期設定です。使用するビューの選択とか、コンフィグ設定とか、そういうのですね。
48行目
MongoDBへの接続情報の記載です。Nodeのようなノンブロッキングサーバでは、サーバの立ち上げ時にだけDB接続し、あとはつなぎっぱなしです。
70行目まではセッションの保存先設定
PHPだとセッションはファイルベース保存が普通ですが、今回はせっかくMongoDBを使うのでセッションをそちらに保存します。

さて、ようやく接続の部分まで来ました……。が、その前にOAuth 1.0aの仕組みをざっと紹介しておきましょう。

OAuth 1.0aの仕組み

  • クライアント(この場合は local.hametuha.pics)がプロバイダ(example.jp)に対してコンシューマーキーやコンシューマーシークレット、コールバックURLなどを指定しつつユーザーをリダイレクト。
  • プロバイダは与えられた認証情報が正しいかをチェックした上で、ユーザーに接続許可を尋ねる。オッケーだったら、コードをつけてリダイレクトする。
  • クライアントはリダイレクトされたきたユーザーにくっついている認証コード(タイムスタンプやらリクエストURIから生成)をプロバイダに確認する。プロバイダはその確認結果をトークンおよびトークンシークレットとして返す。
  • クライアントは認証コードの確認結果であるトークンとトークンシークレットを取得する。以降、そのトークンとトークンシークレットを利用して、クライアントはユーザーがプロバイダ上で行える処理を代わりに行えるようになる。

図にするとこんな感じです。

市役所さながら、いったりきたりするのがOAuth
市役所さながら、顧客をたらい回しにするのがOAuth

世間一般的にはOAuth2.0というもっと新しい方式が採用されていて、有名どころ(Facebook, twitter, google, Github etc.)はみんなそうです。が、基本は1.0aも同じなので、iOSアプリでtwitter連携したりするとき同じようなフローを体験していることでしょう。

これらの処理を自前で実装するとけっこうダルいので、どの言語でもライブラリがあります。twitterやFacebookなどの有名どころだとそれ専用のライブラリがあるのですが、自分のWordPressで実装する場合はもうちょっと下のレイヤーからやっていく必要があります。

Nodeの場合、該当するライブラリはPassportというやつです。

Node系のモジュールはサイトが死ぬほどおしゃれ。
Node系のモジュールはサイトが死ぬほどおしゃれ。

Passportは認証用のライブラリなのですが、様々な方法に対応しています。実際の使用例を見てみましょう。まず、app.jsの先頭でpassportを読み込んでいます。

passport      = require('passport'), // ログイン用モジュール
OAuthStrategy = require('passport-oauth').OAuthStrategy, // PassportのOAuth用モジュール

これら2つを使って、設定を開始します。認証情報はconfig/default.jsonから読み込み。

credentials = {
    wpRoot: configs.get('domain'),
    key   : configs.get('consumerKey'),
    secret: configs.get('consumerSecret')
};
// 認証情報を設定
credentials.requestTokenURL = credentials.wpRoot + '/oauth1/request';
credentials.accessTokenURL = credentials.wpRoot + '/oauth1/access';
credentials.userAuthorizationURL = credentials.wpRoot + '/oauth1/authorize';
// セッションを開始
app.use(passport.initialize());
app.use(passport.session());

続いて、認証処理をまとめてwpという名前で作成。

passport.use('wp', new OAuthStrategy({
        requestTokenURL     : credentials.requestTokenURL,
        accessTokenURL : credentials.accessTokenURL,
        userAuthorizationURL: credentials.userAuthorizationURL,
        consumerKey         : credentials.key,
        consumerSecret      : credentials.secret,
        callbackURL         : configs.get('host') + '/auth/callback'
    },
    function (token, tokenSecret, profile, done) {
        // WordPressのエンドポイントを叩いて情報を取得
        oauthRequest.get('/users/me', token, tokenSecret, function (err, data, response) {
            // 成功したら、dataにはユーザー情報が入っている
            console.log(data);
            if (err) {
                console.log(err);
                return done(null, false);
            } else {
                data = JSON.parse(data);
                data.token = token;
                data.tokenSecret = tokenSecret;
                return done(null, data);
            }
        });
    }
));

コールバックが実行された時点で、トークンとトークンシークレットが取得されているので、138行目ですぐさまそれらのトークンを使ってWordPressにユーザー情報を問い合わせ、現在ログインしているユーザーの情報を取得するという流れです。トークンとトークンシークレットは後で使うので、ユーザー情報に保存しておきます。

oauthRequestという関数は app/lib/oauthRequest.jsに記載されているのですが、自前で実装したモジュールです。OAuthリクエストを行うときはトークンの指定などがあるので、それをラップしたものです。興味のある方は中身を読んでみてください。

では、これらをどこで実行すればよいのでしょうか。とりあえず、/authへアクセスされたら、認証処理を開始するようにしましょう。

// /authで認証処理を開始(WordPressへリダイレクト)
app.get('/auth', passport.authenticate('wp'));

// 認証成功したら/auth/callbackに戻ってくるので、
// 上で作成した認証処理を実行
app.get('/auth/callback', passport.authenticate('wp', {
    successRedirect: '/hello/',
    failureRedirect: '/auth/failure/',
    failureFlash   : true
}));

成功すれば/hello/に移動することになっているので、該当する処理を見てみます。

app.get('/hello', authCheck, function(req, res, next){
    res.render('hello', {
        title: 'こんにちは',
        user: req.session.passport.user
    })
});

views/hello.jadeを読み込んで、userという変数に先ほど取得した(はずの)ユーザー情報を割り当てています。続いて、Jadeの方を見てみましょう。

extends layout
block content
    p こんにちは#{user.display_name}さん!

「こんにちは、高橋文樹さん!」と表示されるようになっていますね。

さて、これが本当に動くのか、Vagrant内で起動しているはずの http://local.hametuha.pics に移動して見てみましょう。ちゃんと動いていれば、WordPressにリダイレクトされて……

OAuthの認証画面
OAuthの認証画面

認証を済ませると元のサイトに戻ってくる……!

あれ、戻ってこない! このあとどうするの?
あれ、戻ってこない! このあとどうするの?

なにやらコードが表示されて終わってしまいました……

これは実を言うと、Passportのバグであって、RedirectURLを指定しても無視されてしまうっぽいのですね。リダイレクトできない環境(iOSアプリとか、コマンドラインとか)では、このように認証コードを表示するのがOAuth 1.0の仕様です。

では、このバグにパッチを当てましょう。実はすでにプルリクを送っているのですが、Travisのビルドがコケたからかなんなのか、まだ採用されていません。このまま永遠に採用されない気もしています。

該当するファイルはapp/node_modules/passport-oauth/node_modules/passport-oauth1/lib/strategy.jsです。

      parsed.query.oauth_token = token;
      utils.merge(parsed.query, self.userAuthorizationParams(options));
      delete parsed.search;
      // As mentiond above, cabllbackURL should be set
      // if confirmed on server response.
      if( params.oauth_callback_confirmed && callbackURL ){
        parsed.query.oauth_callback = callbackURL;
      }
      var location = url.format(parsed);
      self.redirect(location);

これで動くようになるはずです。一応、Ctrl + Cをしてからnpm startでサーバを再起動させてください。

「こんにちは、◯◯さん」がでた!
「こんにちは、◯◯さん」がでた!

それでは、続いてヘッダー右上のメニューボタンから「投稿一覧」というページに移動します。この処理はapp.jsで次のように書かれています。

// 投稿の一覧を取得する
app.get('/posts', authCheck, function(req, res, next){
    res.render('posts', {
        title: 'あなたの投稿',
        user: req.session.passport.user
    })
});

で、views/posts.jadeを見てみると、AngularJSで画面が書かれています。LumxというAngular向けUIフレームワークを使っているので、ごちゃごちゃクラスが書かれていますが、基本的には「postsをすべてリストにして表示する」だけです。

ではそのpostsの取得処理はどうやっているのかというと、app/public/js/_main.jsに書いてあります。

angular.module('hamTop', [
        'lumx'
    ])
    .controller('PostList', ['$scope', '$http', function ($scope, $http) {
        $scope.posts= [];

        $scope.initPosts= function () {
            $http.get('/posts/list/').then(
                function (result) {
                    $scope.posts = result.data;
                },
                function (result) {
                    console.log(result);
                }
            );
        };
    }]);

/posts/listにアクセスして、投稿を取得しているわけですね。SPAを作成する場合、バックエンドでの処理をAngularなどのMVVMフレームワークで叩くのが普通です。これはWordPress本体をSPA化するためにも利用できる手法です。

続いて、Expressが担当しているWordPressとの通信処理を見てみましょう。app.jsにこんな感じで書かれています。

// WordPressに問い合わせる
app.get('/posts/list', authCheck, function(req, res, next){
    var token = req.session.passport.user.token,
        tokenSecret = req.session.passport.user.tokenSecret;
    oauthRequest.get('/posts', token, tokenSecret, function(err, data, response){
        res.json(JSON.parse(data));
    })
});

これにより、Angularの$scope.postsにポストがわたり、うまく行けばこんな感じでWordPressの投稿一覧が取得できます。

WordPressの投稿が取得できました
WordPressの投稿が取得できました

カスタムエンドポイントを作成する

というわけで、WordPressで投稿の一覧を取得することができましたが、ここで「WordPressでできることを他のサイトからする意味なくね?」という疑問が頭に浮かび、俺はなんて無駄な時間を……という三井寿ばりの自責の念が湧き上がってきます。

Automtticはブログホスティングをやっている会社なので管理画面をREST APIでサクサクにすることは意味があるのでしょう。しかし、そうでない場合、WordPressの管理画面にもともと備わっている機能をそっくりそのまま作りなおすことはあまり意味がありません。前述したように……

  • 特定の情報をユーザーから収集することが重要なサイトを作っている(ex. レシピサイト)
  • WebブラウザからアクセスするWordPressからではできない機能を提供するクライアントを作る(ex. スマホアプリ)
  • WordPressおよびPHPでは困難な機能を提供する別のWebサイトを作る(ex. Pythonで自然言語処理)

というような、場合にAPIを利用する価値が出るでしょう。

「管理画面の簡素化」はWP REST APIで実現できる重要な機能の一つです。WordPressの管理画面はけっこうやることが多いので、シンプルな投稿画面を作成してログインユーザの簡便化を図ればコンテンツをたくさん集めることができます。User Generated Mediaを運営している人はこのパターンですね。WordCamp Tokyoで登壇していたNoel TockさんのHappytablesがWP REST API利用してます。

いずれにせよ、WordPressに備わっていない機能が必要なのであれば、それを作らなければいけないですね。

そういう具体例とし、はめぴくっ!はふさわしいのではと思います。

はめぴくっ!では、NodeJSを利用しており、なぜNodeを利用しているかというと、スクリーンショットを取るのが簡単だからですね。Pageresというモジュールがあり、ユーザーに作ってもらったHTMLをスクリーンショットに撮ることで、電子書籍の表紙画像を作成できます。

しかし、現時点では「はめぴくっ!」で作成した画像をユーザーがダウンロードする仕組みになっていて、もう一回WordPressにログインしてアイキャッチをアップロードしなくてはいけません。

これはどうもよろしくないですね。どうせなら作成した画像を自分の作品の表紙画像として設定できたら嬉しいです。ただし、自分のものでない投稿にはアイキャッチを設定できると困ってしまいます。

したがって、要件は次の通りになります。

  • ユーザーははめぴくっ!で表紙画像を作成する
  • ユーザーははめぴくっ!で特定の表紙画像をどの作品集(post_type=series)に設定するか選べる
  • 選択した投稿IDと表紙画像に設定すべきURLをWordPressに投げると、それがアイキャッチに設定される

ここからははめぴくっ!のmasterブランチをベースに「もう機能自体はできている」という前提で説明しますので、これまでに作ったVagrantはもう使いません。vagrant destroy -fなどで捨ててしまってください。

現状のAPIの調査

WordPressのアイキャッチというのは、実際のところカスタムフィールドで、_post_thumbnail_idという名前でメディアのIDが保存されています。しかし、これを一発でやるAPIはないようです。地道にやるとしたら、/mediaにPOSTで投げて登録してから/posts/10/metaにPOSTとかになるんでしょうが、2回もリクエスト投げるのはめんどくさいですね。

あと、アンダースコアで始まるプライベートカスタムフィールドにAPI経由でPOSTできるのかがちょっとわかりません。

それではここでカスタムエンドポイントの追加です! 次のような要件を備えているとしましょう。

  • 利用できるメソッドはPOSTのみ
  • URLを受け取る

要するに /eyecatch/10/ にurl=http://hametuha.pics/hoge/fuga.jpg をPOSTすると、アイキャッチが設定されるというわけですね。

次の場合はエラーになります。

  • 現在のユーザーが該当する投稿の編集権限を持たない
  • URLから画像のダウンロードができない

カスタムエンドポイントを作成する方法は公式ドキュメントAdding Custom Endpointsに書いてあります。

実際のコードはこんな感じです。

// カスタムエンドポイントを登録
add_action('rest_api_init', function(){
    register_rest_route( 'hametuha/v1', '/cover/(?P<id>\\d+)/?', [
        [
            'methods'  => 'POST', // 他にはGETとかDELETEとか
            'callback' => 'hametuha_api_post_cover', // 実行される関数
            'args'     => [ // 引数として渡される数字
                'id' => [
                    'validate_callback' => 'is_numeric', // バリデーション
                    'required' => true, // 必須なら指定
                ],
                'url' => [
                    'required' => true,
                    'validate_callback' => function( $url ) {
                        return preg_match( '#^https?://#', $url );
                    },
                ],
                'title' => [
                    'default' => '', // デフォルト値も設定可能
                ],
            ],
            'permission_callback' => function() {
                // ここでパーミッションを設定
                // 項目自体をなくすとオープンなエンドポイントに
                return current_user_can( 'edit_posts' );
            },
        ],
    ] );
});

/**
 * コールバック
 *
 * @param array $params 上の$argsに指定されたのが入ってくる
 * @return WP_Error|WP_REST_Response このいずれかを返すといい感じにJSONになる
 */
function hametuha_api_post_cover($params){
    if ( ! current_user_can( 'edit_post', $params['id'] ) ) {
        return new \WP_Error( 'permission_denied', 'この投稿を編集する権限がありません。', [ 'status' => 403 ] );
    }
    $post = get_post( $params['id'] );
    // この関数はmedia_sideload_imageという関数を
    // ちょろっと変えたもの
    $attachment_id = hametuha_sideload_image( $params['url'], $post->ID, $params['title'] );
    if ( is_wp_error( $attachment_id ) ) {
        return $attachment_id;
    }
    if ( set_post_thumbnail( $params['id'], $attachment_id ) ) {
        // WP_REST_Responseオブジェクトに配列を渡して
        // 戻せばオッケー
        return new WP_REST_Response( [
            'id' => $post->ID,
            'title' => get_the_title( $post ),
            'status' => $post->post_status,
            'thumbnails' => [
                'full'   => wp_get_attachment_image_src( $attachment_id, 'full' )[0],
                'medium' => wp_get_attachment_image_src( $attachment_id, 'medium' )[0],
            ],
        ] );
    } else {
        // 失敗!
        return new \WP_Error( 'save_failure', '表紙画像の保存に失敗しました。', [ 'status' => 500 ] );
    }
}

そんなに難しいコードではないですが、正規表現の名前付きキャプチャがちょっと面食らうかもしれません。(?P<name>)で囲まれると、名前付きマッチで返ってきます。詳細はサブパターンを参照のこと

$url = '/eyecatch/10';
if( preg_match('#/eyecatch/(?P<id>\\d+)#', $url, $match) ){
     echo $match['id'];
     // 10
}

あと、GETやPOSTでパラメータを受け取る場合、URLから正規表現でキャプチャされた値とともにコールバックにマージされてくるので要注意。くれぐれも$post_id = $_POST['post_id'];とかやらないように!

これでWP REST APIにカスタムエンドポイントを用意することができました。

こんな感じで表紙を作成すると……
こんな感じで表紙を作成すると……
この表紙を設定すべきWordPressの投稿一覧が出てきて……
この表紙を設定すべきWordPressの投稿一覧が出てきて……
こういう画像が設定されていない投稿を選ぶと……
こういう画像が設定されていない投稿を選ぶと……
設定していい? と聞かれて……
設定していい? と聞かれて……
設定された!
設定された!
WordPress側を見ると、こう!
WordPress側を見ると、こう!
ちゃんとアイキャッチに設定されてる!
ちゃんとアイキャッチに設定されてる!

というわけで、お疲れ様でした。実際に動いているかどうか確かめたい方ははめぴくっ!を御覧ください。ただし、破滅派のアカウントがないとログインできません。サーバもしょぼいので、面白半分でたくさん画像を作らないように!

まとめ

というわけで、WP REST APIについてわりとこってり説明しました。

なんというか、「WordPressの未来はJavascriptだ!」とか言われても、普通の人はロリポップでNode動かすの無理だし、結局WordPressのユーザーベースってほとんどはプログラマじゃないわけで、そういう人でもなんとかできたのがいいところだったので、いきなりMVVCとかぶっ込んで大丈夫なのかなと思いますが、まあしょうがないですね。そういう時代なんですよ。

WordCamp Tokyoでも最後に言いましたが、世界は残酷なんですよ。オープンで自由闊達なWebという世界はいっときの夢で、なんかよくわからない様々なツールを使って色々やんなきゃいけないんですよ。みなさんも辛いでしょうが僕も辛いです。

そうそう、個人的にはこのネタで本を一冊書いて電子書籍で出そうと思っています。執筆原稿はGithubで管理し、そのネタでさらにもう一冊書きます。興味ある方は気軽にコンタクトしてください。

明日はファーストクラスでニューヨークまで婚活WordCamp参加しにいったプライムの黄色いおじさん、まがりんです。

[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...)

すべての投稿を見る

高橋文樹ニュースレター

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