【PHP】クラス外から代入も参照もできるprotectedのお話

前エントリを半分翻訳したきたむらです。
それはいつもと変わらない、ある平和な一日の昼下がりの出来事でした。とあるバグを修正しようと調査をしていると、とても不可解な現象が起きました。
Loader::model('user', 'toaru_model'); $user = new ToaruModel(); $user->name = 'taro'; var_dump($user); var_dump($user->name);
(※フレームワークはconcrete5)
と書いてダンプ結果を見ると
object(ToaruModel)[349] protected 'name' => string 'taro' (length=4) public '_dbat' => int 1 public '_table' => string 'ToaruModel' (length=10) public '_tableat' => string 'ToaruModel' (length=10) public '_where' => null public '_saved' => boolean false public '_lasterr' => boolean false public '_original' => boolean false public 'foreignName' => string 'ToaruModel' (length=10) public 'user_id' => null public 'mail' => null public 'created' => null public 'modified' => null array (size=0) empty
という結果になりました。
メンバ変数nameはprotectedであるにもかかわらず'taro'を代入することができ、
さらに、変数nameを直接ダンプすると参照することができます。
しかも、taroはどこかに消えてしまい、何故か空の配列が返ってきたのです。
仮説:protected変数に代入ができたのはToaruModelを継承したクラス内で書いたコードだったから
まず最初に思い当たった可能性がこれでした。
class a { protected $pro = 1; } class b extends a { } class line extends a { static public function foo() { $b = new b(); $b->pro = 2; var_dump($b); } } line::foo();
上記のコードだと
object(b)[348] protected 'pro' => int 2
このようにprotected変数に代入可能です。
ためしに呼び出し元のクラスが対象のモデルを継承しているか確認してみました。
var_dump(is_subclass_of($this, 'ToaruModel')); var_dump(is_subclass_of($this, 'ToaruBaseController'));
boolean false boolean true
…まぁ、コントローラがモデルを継承しているような実装は、よほどロックな人がいないと存在しないはずなのでこの仮説はやはり違いました。
protected変数にアクセスできて空の配列が返却されるまで
さて、ここから謎解きなのですが、
ポイントが三つあります。
- ToaruModelクラスの大元の親クラスはADOdbで、ORMとして利用している
- 紐付けるテーブルにはnameというカラムが存在する
- ToaruModelクラスにprotectedで$name変数を定義している
この時点で怪しい匂いがプンプンするのは分かるのですが、原因がなかなか特定できません。
そこで以下のコードをみてください。
class a { function __get($property) { var_dump('call __get'); return $this->$property; } public function foo(){ $c = 'pro'; $this->$c = 2; } } class b extends a { protected $pro; } $b = new b(); $b->foo(); var_dump($b); var_dump($b->pro);
さて、うまく実行されると思いますか?
結果は以下のようになります。
object(b)[348] protected 'pro' => int 2 string 'call __get' (length=10) int 2
protected変数に外部からアクセスすることができました。
マジックメソッドを使うことによりオブジェクト指向の向こう側に辿り着いたのです。
このページを読むと"現在のスコープからはアクセス不能なプロパティやメソッドを操作しようとしたとき"に__get()が呼び出される、とあります。
外部からアクセスできないprotected変数を参照しようとして、__get()が呼び出され、結果$this->"pro"の内容を返却した。という流れになります。
これを踏まえた上で、ADOdbの実際のコードを覗いてみましょう。
__get()が定義されており、そこで呼び出されるLoadRelations()内の処理で、どの分岐条件にも当てはまらなければarray()が返却されます。
よってToaruModelのprotected変数nameを呼び出したとき、
変数nameは現在のスコープからはアクセス不能なプロパティなのでToaruModelの継承元のADODB_Active_Recordクラスの__get()が呼ばれ、その結果、空の配列が返却された、という現象が起きたのでした。
protected変数に代入できたわけ
ここまでくると、代入できた理由も簡単に想像できると思いますが、その通りです。
マジックメソッド__set()関数が悪さをしていたのです。
さきほどのサンプルコードに__set()を追加して検証してみましょう。
class a { function __get($property) { var_dump('call __get'); return $this->$property; } function __set($name, $value) { var_dump('call __set'); $this->$name = $value; } public function foo(){ $c = 'pro'; $this->$c = 2; } } class b extends a { protected $pro; } $b = new b(); $b->pro = 3; var_dump($b); var_dump($b->pro);
string 'call __set' (length=10) object(b)[348] protected 'pro' => int 3 string 'call __get' (length=10) int 3
ご覧の通りprotected変数に代入することもできました。
もう一度ADOdbの実際のコードを覗いてみましょう。
__set()関数が定義されており、引数に指定した文字列の変数に引数の値を代入する処理になっています。
よってToaruModelのprotected変数nameに値を代入したとき、
変数nameは例のごとく現在のスコープからはアクセス不能なプロパティなのでToaruModelの継承元のADODB_Active_Recordクラスの__set()が呼ばれ、その結果、protected変数nameに普通に代入ができた、という現象が起きたのです。
以上で平和な一日に起きた不可解な現象を解明できました。
めでたしめでたし。