fbpx

メニュー

wpdb道場 (9) 〜リファクタリングしよう〜

高橋文樹 高橋文樹

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

さて、この連載の反響もほとんどなくなってきましたが、僕はがんばりますよ。

前回まででフォローとアンフォローをできるようになりました。で、今後はフォロワーを眺めてうっとりする画面を作りたいな、と思うんですが、その前にいろいろと考え直してみたいことがあります。

現時点で見えている課題

1. モデル増えるんちゃうん?

Freundschaft\Model\Followersというクラスを作りましたが、今後、テーブルは増えないのでしょうか。いや、増えます! メッセージを登録したいからです!

となると、似たような処理は共通化したいですね。どうしましょう。

2. Ajax増えるんちゃうん?

Ajaxの実装はfreundschaft.php内で行っていますが、正直面倒くさいですよね。

  1. add_actionでAjaxのフックを登録
  2. Ajaxの処理を行う関数を実装
  3. JSにそのエンドポイントを教える

これを毎回やっていると、いつか間違えますね。できれば、一箇所にコードを書くだけでこのすべてが一発で実装されるようにしたいところ。

 

では、これらのものをやってみましょう。

モデルをリファクタリング

さて、前回作ったモデルですが、リファクタリングしたいなーと思うのは上にあげたポイントです。

では、そのための抽象クラスを作りましょう。

まず、Freundschaft\Pattern\Modelという抽象クラスを作ります。で、名前がかぶってしまったので、Freundschaft\Modelという名前空間をFreundschaft\Modelsという複数形に変えます。

新しい抽象クラス
新しい抽象クラス

テキストエディタの方はディレクトリの名前を変更するのと、Freundschaft/Models/Followers.php内の名前空間宣言を変える必要がありますね。

あと、freundschaft.phpにもFollowersクラスを呼び出している箇所があるので、これを直します。

PhpStormなどのIDEなら、コンテキストメニューで”Refactor -> Move”とかやるだけで終わりだと思います。

リファクタリングすれば関係するところが全部変わる
リファクタリングすれば関係するところが全部変わる

どうでしょう、いったんこの段階でそのまま動くかどうか、試してみてください。動きましたね。

メソッドの移動

まず最初にFollowersクラスのメソッドを移していきます。まずマジックメソッドの__getから。テーブル名とwpdbを取得するだけなので、これは共通でいいですね。

テーブル名を自動で決める

さて、テーブル名は$this->nameと$this->db->prefixを組み合わせて作っていますが、$this->nameって自動で決められませんかね。

たとえば、Followersクラスなら$this->nameは自動的にfollowersでよいような気がします。TransactionLogsクラスなら、transaction_logsとか。

初期値は空にしておいて、その場合はクラス名から決定。もしどうしても変えたい場合は$nameを上書きする自由を与えてあげましょう。

どうでしょう、これで「賢いデフォルト」ができたのではないでしょうか。

それでは、このためのコードを実装します。まず、Freundschaft\Patter\Modelのコンストラクタで自分の名前を決めます。Singletonを継承しているので、コンストラクタはprotectedですね。

/**
 * Constructor
 *
 * @param array $settings
 */
protected function __construct( array $settings = array() ) {
     // Set table name
     if( empty($this->name) ){
          $name = explode('\\', get_called_class());
          $this->name = $this->decamelize($name[count($name) - 1]);
     }
}

get_called_class()によって取得した完全クラス名(Freundschaft\Models\Followers)の末尾をとって、それをテーブル名に変換します。で、変換するためのメソッドdecamelizeはこれから実装します。

/**
 * Make upper camel to snake case
 *
 * @param string $string
 *
 * @return string
 */
protected function decamelize($string){
     return strtolower(preg_replace_callback('/(?<=.)([A-Z])/', function($match){
          return '_'.strtolower($match[1]);
     }, $string));
}

正規表現で先頭以外にある大文字を小文字に変換して、前にアンダースコアをつける感じですね。(?<=.)の部分は「あと読み言明」という書き方です。 どうでしょう、これでいちいちテーブル名を書かなくてよくなったんではないでしょうか。

$wpdbを便利にする

続いて、$wpdbを少し便利にしてみましょう。 $wpdb->updateや$wpdb->insertはカラム名と値からなる配列と、各値のプレースホルダーの配列を受け取ります。udpateメソッドは最大で4つの配列を受け取ります。

今回作っているwp_followersテーブルとかはカラムが3つしかないのでいいのですが、カラムが8個とかになったらこんな風になります。(※カラム名は適当です)

$wpdb->update($hoge_table, array(
     'foo' => 'var',
     'hoge' => 'fuga',
     'title' => 'var',
     'parent_id' => 2,
     'max_children' => 20,
), array(
     'status' => 'publish',
     'type' => 'any',
), array('%s', '%s', '%s', '%d', '%d', ), array('%s', '%s', ));

あ、あほらしい……。これは以下の意味であほらしいですね。

  • プレースホルダーの配列はテーブルを定義した段階で自明。なので、実行するたびに指定する必要はない。
  • タイポの危険性が高い。%sと%d、%fが連続する配列なんて、普通に間違える。
  • 配列の順番を変えたら壊れそう。

もっとも、$wpdbは単なるデータベースアクセスを抽象化しただけのクラスなので、「カラムの値の型ぐらい知ってろよ」と言われても「知らんがな」という話でしょう。

したがって、各Modelクラスは必ず、$columnsという配列を持ち、ここにカラム名とプレースホルダーの組み合わせを持つようにします。Followersクラスならこんな感じ。

protected $columns = array(
     'follower_id' => '%d',
     'user_id' => '%d',
     'created' => '%s',
);

これを使えば、次のような配列から、$wpdb->updateメソッドが期待する配列を取得できますよね。

array(
     'user_id' => 1,
     'follower_id' => 2,
     'created' => '2015-01-22'
)
// => array('%d', '%d', '%s')

では、これをPattern\Modelに実装してみましょう。

/**
 * Get values and wheres
 *
 * @param array $data
 *
 * @return array ['values' => [], 'wheres' => []]
 * @throws \InvalidArgumentException
 */
protected function getValueAndWhere( array $data ){
     if( empty($data) ){
          throw new \InvalidArgumentException('配列が空です。', 500);
     }
     $values = array();
     $wheres = array();
     foreach( $data as $column_name => $value ){
          // Throw exception if invalid columns name is past.
          if( !isset($this->columns[$column_name]) ){
               throw new \InvalidArgumentException(sprintf('存在しないカラム%sが指定されています。', $column_name), 500);
          }
          // Create wheres.
          $values[$column_name] = $value;
          $wheres[] = $this->columns[$column_name];
     }
     return compact('values', 'wheres');
}

配列が空、もしくは定義されていないカラムを指定するとエラーを投げるという男前の仕様になっています。何も準備をしていない(=try-catchしていない)と真っ白になります。かっこいいですね。

戻ってきた配列はvaluesとwheresをキーに持ちます。

さらに、要件を追加しましょう。

Followersクラスは基本的にwp_followersを対象としています。なので、テーブル名を指定する必要はありません。もしFollowersクラス内から別のテーブルを操作したいとなった場合、それは「ルール違反」と考えるようにしましょう。

このルールに従い、$this->db->deleteのラッパーメソッドとして、$this->deleteを実装しましょう。

// もともとのdeleteメソッド
$this->db->delete($this->table, array(
     'follower_id' => 1,
     'user_id' => 2,
), array('%d', '%d'));
// 新しいdeleteメソッド
$this->delete(array(
     'follower_id' => 1,
     'user_id' => 2,
));

このdeleteメソッドの定義はこんな感じになります。

/**
 * Delete
 *
 * @param array $where
 *
 * @return false|int
 */
public function delete( array $where ){
     $wheres = $this->getValueAndWhere($where);
     return $this->db->delete($this->table, $wheres['values'], $wheres['wheres']);
}

whereが被りましたね。まあいいでしょう。似たような感じでinsertとupdateも実装します。

その他

全部書いているときりがないので、Modelに関してリファクタリングした内容を箇条書きにしてみます。

  • マルチサイトの場合、多くのテーブルはブログの数だけできますが(wp_2_options, wp_3_posts)、場合によってはサイト全体で一つにしたい場合もあるでしょう。そういう場合、$unique_on_multisite = trueとすると、テーブル名がユニークになります。
  • あとで実装するユニットテスト用のメソッドtestSettingを追加。$columnsが指定されていないとか、そういうのを判定できるように。
  • プリペアドステートメントを使う場合、$this->db->get_var($this->db->prepare($query, $var))と書かなくてはいけないが、「安全性の高い方法の記述コストが高い」というのは良くないので、クエリ発行系のメソッド全部のシンタックスシュガーを用意し、引数が2つ以上ある場合は自動的にprepareメソッドをかぶせる。
  • $timestamp_on_createや$timestamp_on_updateという変数にカラム名を書くと、insertやupdateのときに自動で追加してくれる。

どうでしょう。この差分はGithubのコミットログで見てみてください。どこがどう変わったか、探してみよう!(投げやり)

Ajaxの部分をリファクタリング

さて、モデルだけでもかなり大変だったのですが、Ajax部分も修正しましょう。Modelと同じく、Ajaxも抽象クラスがあった方がいいですね。

abstract class Ajax extends Singleton
{
}

モデルのときは具体的なFollowersクラスから抽象的なModelクラスへと実装を分離していきましたが、今回は逆に抽象的な部分からいきます。

おさらいになりますが、WordPressでAjaxを行うときは、次のような手続きを踏みます。

  1. wp_ajax_*またはwp_ajax_nopriv_*フックに関数を登録
  2. wp-admin/adimin-ajax.php?action=*にリクエストすると、登録された関数が起動するので、実装しておく
  3. Ajaxを呼び出す側のページにJS(必要ならCSSも)を読み込み
  4. 場合によってはwp_localize_scriptでJSに変数を渡す

この通り、主に4つの仕事があります。できる限り共通化したいので、こんなルールにしましょう。

Pattern\Ajaxクラスのサブクラスのうち、ajaxまたはajaxNoprivではじまるpublicな関数は全部Ajaxとして登録される。

  • 1つのクラスが1つのJSを読み込む。assets/js/ajax/にクラス名をハイフンにした名前のJSが該当(MyAjax => my-ajax.js)。
  • JSには必ず変数が渡される
  • CSSの読み込みは任意
  • nonceのキーはクラス全体で1つ。どのエンドポイントでもこのnonceをチェックする

このようなルールにします。このルールはパフォーマンスを犠牲(ex. 必要のないJSが読み込まれる)にしますが、そこは割り切ります。

いままでfreundschaft.phpに書いていた処理の一部をPattern\AjaxとAPI\Ajax\Followに分けます。ちょっと説明する量が多いので、githubのdiffでどうぞ。

さて、この中でこの過程でこんな事態が発生しました。

クラス名をアンダースコアにするメソッドdecamelizeをModelに定義したが、ほかでも使うことがわかった。

とりあえず、Singletonのに移したが、Singleton以外でも使うかもしれないですね。こんな状況のとき、どうしましょう。PHP5.4ならtraitという機能があるのですが、今回は別のアプローチを取ります。

小さな機能のまとまりをヘルパーとして実装してみよう

CodeIgniterやCakePHPといったフレームワークには「ヘルパー」という機能があります。たとえば、セッション周りの関数を使いたいなーというとき、ヘルパーをオンにしておくと、 $this->session->write('hoge') みたいな感じで使えます。decamelizeなんかはうってつけですね。

今回はstringヘルパーということで、作ってみましょう。中身になにかを保持しない限り、基本的にSingleton実装で構いません。Utilityフォルダを作って、Singletonのサブクラスで実装してみてください。

このインスタンスをModelのゲッターに実装してみましょう。そうすると、いままで$this->decamelizeと書いてあったのが、$this->string->decamelizeになります。(diff

他にもいろいろ候補はあります。そうですね、Ajax\Followクラスの中にある処理で、こんなのがありますね。

if( isset($_POST['author_ids']) && is_array($_POST['author_ids'])){
}

PHPでは、$_POST[‘hoge’]と書いたときにhogeが存在しないとエラーログが出ます。わりとみなさん気づかないでやっているのですが、普通はissetで値の存在を確認してから取得を試みます。

しかし、毎回issetとか書くのめんどくさいので、いきなり$this->input->getとやったら取れるのが楽ですね。何もない場合はnullが返ってくるという仕組みにして、Singletonクラスとして実装してみましょう。

あと、モデルもいちいち呼ぶの面倒くさいですね。$this->models->followとかやったら取れるのがよくないですか。

で、全部書いたのがこちら

かなり整理されてきましたね。やたらファイルが多いなーという気がしないでもないですが、1ファイルが異常に長くなるよりはましです。

まとめ

  • リファクタリングして、共通の処理を抽象化しようね
  • 自明なことは書かないで済むようにしよう
  • ヘルパーという仕組があるよ

さて、ぶっちゃけた話、この一連の連載はあまり反響がないので、モチベーションが下がってきました。githubにあげているので、ちまちまコードを紹介する必要もないでしょう。

次回は少し趣向を変えて、テスト用データのインポートなど、実運用に近い部分のノウハウを紹介します。そのうちユニットテストもやるよ。

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

すべての投稿を見る

高橋文樹ニュースレター

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