PHPでリフレクション
こんにちは、小川です。たまには文句でも書いてみます。
先日ふと「スパイシーチキン」という単語を聞いて、昔のアシアルブログを思い出しました。
スパイシーチキンという単語を便りに検索をしてみると、スパイシーチキンの絶頂期は2006年から2007年の初めごろ。
僕は2006年の12月18日にアシアルにアルバイトとして入社したため、スパイシーチキン時代のアシアルを知っています。
むしろ、スパイシーチキンがあったからこそアシアルに入社したといっても過言ではありません。
しかし、今はどうでしょう。日常が垣間見えるのは、おおよそ月に1度投稿される阿部さんのブログくらい・・・。
symfonyの話しかしない人もいるし。そんなだからいつまでたっても彼女ができないんですよ!
ああ、、、あの頃のアシアルブログはどこへいってしまったのでしょうか。
やはりここはアシアルブログ再建のためにも、再度スパイシーチキンに登場してもらうほかありません!
ちなみに僕は、はらどけいのお弁当はチキンカツ派なので、誰かよろしくお願いします。
というわけで今回はPHPでリフレクションを扱う例をいくつかご紹介したいと思います。
リフレクションとは、Wikipediaによると次のように説明されています。
情報工学においてリフレクション (reflection) とは、プログラムの実行過程でプログラム自身の構造を読み取ったり書き換えたりする技術のことである。
PHPではPHP 5以降、ReflectionClassやReflectionMethodというクラスがコアに含まれています。それらを使ってクラスに関する様々な情報を読み込むことや情報を書き換えたりすることが可能です。
■ サンプルコードによるリフレクションの説明
それではサンプルコードを通じて使い方の説明をしたいと思います。はじめにサンプルとしてEntity\Userクラスを定義します。なお、手元にPHP 5.3しかないのですべてPHP 5.3上で動かしたものになります。
<?php // Entity/User.php namespace Entity; class User { private $name; private $gender; public function __construct($name, $gender) { $this->name = $name; $this->gender = $gender; } public function getName() { return $this->name; } private function getGender() { return $this->gender; } }
名前を保持するnameプロパティと、性別を保持するgenderプロパティを定義します。genderプロパティに対するゲッターメソッドはprivateにしてあります。
■ ReflectionClass
まずクラスの情報を抜き出してみましょう。クラスの情報を取得したい場合はReflectionClassクラスを用います。ReflectionClassのコンストラクタにはクラス名を指定します。
<?php require __DIR__.'/Entity/User.php'; $reflClass = new ReflectionClass('Entity\User'); echo sprintf("%-20s: %s\n", 'getName', $reflClass->getName()); echo sprintf("%-20s: %s\n", 'getNamespaceName', $reflClass->getNamespaceName()); echo sprintf("%-20s: %s\n", 'getShortName', $reflClass->getShortName()); echo sprintf("%-20s: %s\n", 'getFileName', $reflClass->getFileName());
getName : Entity\User getNamespaceName : Entity getShortName : User getFileName : /home/fivestar/asial-blog/Entity/User.php
ReflectionClassクラスはクラスの定義情報を読み込んで様々な情報にアクセスすることが可能です。定数やプロパティ、メソッド情報の取得、親クラスや実装しているインターフェイスの取得、finalかabstractかの判定、クラスが定義されているファイルのパスの取得、インスタンスの生成など、本当に様々です。(PHP: ReflectionClass - Manual)
インスタンスを生成するにはnewInstance()メソッドを実行します。newInstance()メソッドの引数がそのままコンストラクタの引数にあたります。
# newInstanceArgs()メソッドもあり、こちらはコンストラクタに渡す引数を1つの配列にまとめて指定します。call_user_func()とcall_user_func_array()の関係とほぼ同じです。<?php require __DIR__.'/Entity/User.php'; $reflClass = new ReflectionClass('Entity\User'); $user = $reflClass->newInstance('fivestar', 'male'); var_dump($user);
object(Entity\User)#2 (2) { ["name":"Entity\User":private]=> string(8) "fivestar" ["gender":"Entity\User":private]=> string(4) "male" }
■ ReflectionMethod
リフレクションはメソッドでも行えます。メソッドの場合はReflectionMethodクラスを用います。ReflectionMethodクラスのコンストラクタにはクラス名とメソッド名を指定します。
<?php require __DIR__.'/Entity/User.php'; $reflMethod = new ReflectionMethod('Entity\User', 'getName'); echo sprintf("%-20s: %s\n", 'getName', $reflMethod->getName()); echo sprintf("%-20s: %s\n", 'isPublic', $reflMethod->isPublic() ? 'true' : 'false'); echo sprintf("%-20s: %s\n", 'isPrivate', $reflMethod->isPrivate() ? 'true' : 'false'); echo sprintf("%-20s: %s\n", 'isStatic', $reflMethod->isStatic() ? 'true' : 'false'); echo "\n";
[ReflectionMethod] getName : getName isPublic : true isPrivate : false isStatic : false
これもReflectionClassクラスと同様、様々な情報が取得可能です。
また、ReflectionMethodクラスに指定したメソッドを実行することも可能です。ReflectionMethodクラスのinvoke()メソッドを用います。
<?php // ... $user = new Entity\User('fivestar', 'male'); echo sprintf("%-20s: %s\n", 'invoke', $reflMethod->invoke($user));
invoke : fivestar
invoke()メソッドの第1引数には、メソッドが属するクラスのインスタンスを指定します。第2引数以降には引数を指定します。これもinvokeArgs()メソッドがあり、1つの配列による引数の指定も可能です。
ただしprivateなメソッドは実行できません。
<?php require __DIR__.'/Entity/User.php'; $reflMethod = new ReflectionMethod('Entity\User', 'getGender'); echo sprintf("%-20s: %s\n", 'invoke', $reflMethod->invoke($user));
PHP Fatal error: Uncaught exception 'ReflectionException' with message 'Trying to invoke private method Entity\User::getGender() from scope ReflectionMethod'
しかし、setAccessible()メソッドを使うとprivateやprotectedなメソッドも実行可能になります。
<?php // ... $reflMethod->setAccessible(true); echo sprintf("%-20s: %s\n", 'invoke', $reflMethod->invoke($user));
invoke : male
setAccessible()でアクセス可能にしたからといって、privateなメソッドがpublicになるわけではありません。あくまでprivateな状態で実行可能になる、ということです。これを用いてprivateメソッドのテストを行うこともできるようになります。
なお、setAccessible()メソッドはPHP 5.3.2で実装されたので、かなり新しいバージョンのPHPでないと使えません。Symfony2が5.3.2以上のみ対応しているのもこの辺りが要因かと思われます。
ちなみにReflectionMethodクラスはReflectionFunctionAbstractクラスの子クラスになります。関数用のReflectionFunctionクラスもあり、同様にReflectionFunctionAbstractクラスを継承しています。
ReflectionClassクラスを継承したReflectionObjectクラスもあり、こちらはインスタンスを受けとります。
このほかにもメソッドなどの引数を表わすReflectionParameterクラスや、クラスのプロパティを表わすReflectionPropertyクラスなどが用意されています。
リフレクション関連のクラスはReflectorインターフェイスを実装しています。■ リフレクションの利点
さて、リフレクションについて簡単にご説明しましたが、これを使うと何が嬉しいのでしょうか?正直なところ、普通にプログラムを組んでいて使うことは滅 多にないのかなと思います。
使いどころとしてまず挙げられるのはsetAccessible()メソッドによるprivateメソッドのテストでしょうか。フレームワークの内部で利用されることは割と多いかと思います。特定のディレクトリへのパスを取得するため、あるクラスの定義されたファイルからパスを逆算する、ということをしているコードは何度か見たことがあります。
■ Symfony2での使われ方
Doctrine2やSymfony\Component\Validatorはアノテーションを用いてメタデータのマッピングを行いますが、これもリフレクションを使って行っています
。具体的に言うと、ReflectionClassクラスやReflectionMethodクラスにはgetDocComment()メソッドがあり、クラスやメソッドのドキュメントコメントを読み込んでパースしています。
これはDoctrine\Common\Annotations\AnnotationReaderクラスで行われています。またこれらのライブラリではリフレクションを用いて、データベースから取得した情報やユーザの入力情報をオブジェクトのプロパティに無理やりねじ込むということも行っています。
ReflectionPropertyクラスにもsetAccessible()メソッドがあり、これを用いてprivateなプロパティにアクセスしています。これはDoctrine\ORM\Mapping\ClassMetadataクラスで行われています。他にも、Symfony\Bundle\FrameworkBundle\Controller\ControllerResolverクラスでは、ReflectionParameterクラスを用いて引数として定義された変数名を抜き出して処理をするといったことも行っています。
■ ReflectionAnnotation
これはまだRFCですが、ReflectionAnnotationというクラスの話が挙がっています。RFCということで紹介だけ。
----
インターフェイスを実装しているかなんてのはinstanceof演算子を使えばよいですし、メソッドが定義されているかはmethod_exists()で十分です。
とはいえ特殊なことをやりたいというケースも少なからずあります。また、フレームワークなどのソースコードを読む際にリフレクションの存在を知っておくと理解の幅が広がるでしょう。