久々の更新です。
ネタは溜まっているんですがなかなか書くモチベが沸かず。
これから定期的に更新できるよう頑張ります。
今日はPHPのマジックメソッドについて書きます。
PHPのマジックメソッドの中に__callStatic
というメソッドがあります。
これは、クラスで定義されていないメソッドに対してstaticなコールをした際に呼び出されるフックです。
この機能を使えば、Rubyでいうところのmethod missing
のような挙動が可能になるのでは?
と考え、実験にRuby on Railsで以前まで使われていたfind_all_by_*
を実装してみたいと思います。
ライブラリ等に依存しないシンプルなデモと、
実用化するために、FuelPHP
のモデルを用いた例も作成します。
__callStatic() は、 アクセス不能メソッドを静的コンテキストで実行したときに起動します。
引数 $name は、 コールしようとしたメソッドの名前です。 引数 $arguments は配列で、メソッド $name に渡そうとしたパラメータが格納されます。
— PHP: オーバーロード – Manual
サンプルコードを呼んでみると、__callStatic
はPHP 5.3以上で動作可能な機能のようです。
引数の説明が分かりにくいので、コードを交えつつ解説します。
<?php
class Sample {
public static function __callStatic($name, $args) {
var_dump($name, $args);
}
}
Sample::hogehoge(1,2,3,4,5);
こんなコードがあったとします。
Sample::hogehoge()
をコールすると、Sampleクラスにはhogehogeメソッドが定義されていないため、__callStatic
が呼び出されます。
このとき、$nameにはメソッド名であるhogehoge
が、$argsにはhogehogeに渡した引数である[1, 2, 3, 4, 5]
が配列で格納されています。
これを利用して、find_all_by_*
を実装してみたいと思います。
先ほどから出ているfind_all_byメソッドとは何でしょうか。
やや古いRails(3.2.*)まで使用されていたデータ検索用のメソッドです。
カラム名を指定して、検索条件にあうすべてのレコードを取得する。
rails4からは、whereで代替することができる。
— find_all_by - リファレンス - Railsドキュメント
4.*系からは非推奨になってしまいました。
とはいえ、今回はあくまで__callStaticが活躍できそうな方法を探ることが目的なので構わず実装します。
単一のカラムの指定も受け付け、
更に_and_
で繋いだ複数カラムと値の組み合わせを受け付ける
というものを作ってみたいと思います。
例えば、クラス名::カラム名1_and_カラム名2(値1, 値2)
とメソッドを呼び出せば、__callStatic内では
SELECT * FROM クラス名 WHERE カラム名1=値1 AND カラム名2=値2
と解釈してもらうことにします。
なお、このメソッドでは、カラム名にand
が含まれることを考慮しません。
まずは、2つのクラスを作成します。
<?php
abstract class Model {
protected static $table_name = null;
}
class Test extends Model {
protected static $table_name = 'test';
}
Model
クラスは抽象クラスで、継承される前提のクラスです。
Test
クラスはModelクラスを継承し、実際にアプリケーション内で使用されるモデルクラスとします。
テーブル名は、クラス名から取得などをせずに、
おとなしく$table_name
というプロパティを定義しています。
マジックメソッドの前に、汎用的な検索メソッドを作成します。
連想配列を受け取り、それをSQL文字列に変換するメソッドです。
Test
クラスにはこれ以上書くことがないので、以下のコードではModel
クラスのみ記述します。
class Model {
const FIND_ALL_BY = 'find_all_by_';
protected static $table_name = '';
// 連想配列を与えて、条件にマッチする行を全件取得するSQLを作成する
public static function find_all($where = array()) {
$sql = 'SELECT * FROM '.static::$table_name;
// 連想配列が指定されていたらWHERE句を生成
if(count($where) > 0) {
$keyval = array();
foreach($where as $column => $value) {
$keyval[] = $column.'='.$value;
}
$sql .= ' WHERE '.implode(' AND ', $keyval);
}
return $sql;
}
find_all
というメソッドを追加しました。
"コピペで使えるコード"を目指しているわけではないので、SQLの生成自体はかなり適当です。
クオートしていないので、クオートが必要な値が含まれていたら実行すらできません。
しかし、イメージは十分に伝わると思います。
SQLの作成が雑なのは、自力でSQLを生成するのは非現実的であることと、
この後FuelPHP対応の実用版を書くため、カラム名と値の組み合わせという情報さえ手に入れば十分だからです。
ちなみに、findAll
メソッドの使用イメージはこんな感じです。
// "SELECT * FROM test WHERE id=1"
Test::find_all(array('id' => 1));
// "SELECT * FROM test WHERE name=Leko AND age => 22"
Test::find_all(array('name' => 'Leko', 'age' => 22));
本題です。マジックメソッドを入れていきます。
先ほどfind_all
メソッドを作成したので、検索自体はfind_all
の責務です。
そのため__callStatic
では、カラム名と値の組み合わせさえ取得できればOKです。
find_all
へパスする処理を実装します。
全体を書くとやや長くなるため、マジックメソッドのみ記述します。
public static function __callStatic($method_name, $args) {
// メソッド名がfind_all_by_で始まる場合のみ解析を行う
if(strpos($method_name, self::FIND_ALL_BY) === 0) {
// find_all_by_を除去
$method_name = str_replace(self::FIND_ALL_BY, '', $method_name);
// カラム名 => 値の連想配列へ変換
$columns = explode('_and_', $method_name);
$where = array_combine($columns, $args);
return self::find_all($where);
}
}
find_all_by_*
(※self::FIND_ALL_BY
)の書式なら、
_and_
で千切って配列化し、
カラム名 => 値
の連想配列へ変換し、
という処理になっています。
他のマジックメソッドの邪魔をしないために、
呼び出されたメソッドの名前が、find_all_by_
で始まる場合のみ、処理するようにしています。
使用イメージは以下の通りです。
// "SELECT * FROM test WHERE id=1"
Test::find_all_by_id(1);
// "SELECT * FROM test WHERE name=Leko"
Test::find_all_by_name('Leko');
// "SELECT * FROM test WHERE name=Leko AND age=22"
Test::find_all_by_name_and_age('Leko', 22);
// "SELECT * FROM test WHERE name=Leko and created_at=2014-07-20 04:00:00"
Test::find_all_by_name_and_created_at('Leko', date('Y-m-d h:i:s'));
created_at
のように、カラム名にアンダースコアが混じっていても問題ありません。
本題2です。上記のマジックメソッドを実用化してみます。
FuelPHPを選んだ理由は、
メソッド名にアンダースコアを使うことをコーディング規約にしているのと、
普段業務で使用しているため勝手がわかるという理由からです。
また、自作したfind_all
メソッドは、
同等以上の機能がFuelPHPのfind_by
メソッドで実現されているためそちらを利用します。
また、find_by_*
というメソッドもサポートしています。
・・・気にせず実装します。
_and_
があれば差別化できますし、車輪の再発明だとしても、勉強のためです。
マジックメソッドをFuelPHPのモデルに組み込むと、下記のようになります。
abstract class Model_Base extends Model_Crud
{
const FIND_ALL_BY = 'find_all_by_';
protected static $_table_name = 'hoges';
public static function __callStatic($method_name, $args) {
// メソッド名がfind_all_by_で始まる場合のみ解析を行う
if(strpos($method_name, self::FIND_ALL_BY) === 0) {
// find_all_by_を除去
$method_name = str_replace(self::FIND_ALL_BY, '', $method_name);
// カラム名 => 値の連想配列へ変換
$columns = explode('_and_', $method_name);
$where = array_combine($columns, $args);
return self::find_by($where);
}
}
}
find_all_by_
以外のメソッドには反応しないので、
FuelPHPの他のメソッドを邪魔することはありません。
Railsは数回しか触ったことがなく、特に好きも嫌いも無いんですが、
とか言われると、「意外と色々な機能あるよ」と悲しい気持ちになるので、今回の記事に至りました。
確かにイケてないし文法や言語仕様の欠陥はどうしようもないですが、
今回の記事のように、Rubyちっくな機能も作れます。ダメダメではないよ!
ただ、当然ながらマジックメソッドを使用すると動作速度に影響しますし、
あまりトリッキーなことはやらないほうが身のためかもしれません。