◆ JavaScriptっぽいことをさせてみます

◆ メソッドチェーンではなくプロトタイプチェーンです 

PHPでプロトタイプチェーンをさせたい!
PHPの __get__call 使えば行けそうな気がしたので勢いで作ってしまいました

PHP、JavaScript化計画

まずこれが基本となる Object のコンストラクタ(クラス)です
class Object{
function __call($name, $arguments){
if(is_object($this->__proto__)){
$meth = $this->__proto__->$name;
array_unshift($arguments, $this);
return call_user_func_array($meth, $arguments);
}else{
throw new Exception();
}
}
function __get($name){
if(is_object($this->__proto__)){
return $this->__proto__->$name;
}else{
return null;
}
}

function __construct($arr = null){
global $Object_prototype;
if(isset($Object_prototype)){
$this->__proto__ = $Object_prototype;
}else{
$this->__proto__ = null;
}
if(is_array($arr)){
foreach($arr as $key => $val){
$this->$key = $val;
}
}
}
}
$Object_prototype = new Object();
$Object_prototype->toString = function($This){
return json_encode($This);
};

では使ってみます
class Sample extends Object{
function __construct($data){
global $Sample_prototype;
$this->__proto__ = $Sample_prototype;
$this->data = $data;
}
}
$Sample_prototype = new Object();
$Sample_prototype->a = 200;
$Sample_prototype->double = function($This){
return $This->data * $This->data;
};
$Sample_prototype->plus = function($This, $num){
return $This->data + $num;
};


$s = new Sample(10);
var_dump( $s->a );
var_dump( $s->double() );
var_dump( $s->plus(1) );
int(200)
int(100)
int(11)


簡単に解説します


新しいクラス Sample を作るわけですが 新しく作るクラスにはすべて Object を継承させるようにします
__get __call のマジックメソッドを使うためです
この2つは 自分のプロパティやメソッドがなかった場合に呼ばれるものです
プロトタイプチェーンでは 自分のプロパティがなければ __proto__ プロパティを辿ってプロパティチェックを繰り返します
__get __call ではそれと同じ動きをするようにしてあります
__proto__ プロパティを最後まで辿ってもプロパティがみつからないと null が返って来ます
メソッド呼び出しだと null を関数呼び出しするわけなので 例外を投げるようになっています

JavaScriptだとクラスを作るのは関数作るのと一緒です
それに合わせて クラス作るときはコンストラクタだけを宣言します
また PHP では クラスを作っても prototype なんて作られません
なので 自分で作ります
Sample クラスを作ったなら $Sample_prototype というものを自分で作っています
元は Object のインスタンスで これに必要なプロパティを付け足したものになります
メソッドはなく プロパティに無名関数を入れます
この辺も JavaScript らしさがでますね

詳しくは最後で書きますが prototype に入れるメソッドは第一引数に $this を受けることになります
変数名はなんでもいいですが それっぽく見えるようにサンプルでは $This としてます
$this とはできないので注意です

では Sample のインスタンスを作って メソッドやプロパティを表示してみましょう
a$Sample_prototype で設定されたプロパティです
Sample のインスタンス $s には存在しないはずですが $s->a で参照できています

$s->double もインスタンス生成時に渡して 10 x 10 の結果が表示されています
2倍じゃなくて2乗です
こちらも double というメソッドは $Sample_prototype のプロパティにあるものです
最後の plus も同じようにできています

PHPなので -> をつかってますが 使い方はほとんど JavaScript ですよね

プロトタイプチェーンを書き換える

では次は クラスを使わずにもっと単純にプロトタイプチェーンをつないでみます
$a = new Object(["a" => 1]);
$b = new Object(["b" => 2]);
$c = new Object(["c" => 3]);
$a->__proto__ = $b;
$b->__proto__ = $c;
var_dump($a);
var_dump($a->a);
var_dump($a->b);
var_dump($a->c);
var_dump($a->toString());
object(Object)#7 (2) {
["__proto__"]=>
object(Object)#8 (2) {
["__proto__"]=>
object(Object)#9 (2) {
["__proto__"]=>
object(Object)#1 (2) {
["__proto__"]=>
NULL
["toString"]=>
object(Closure)#2 (1) {
["parameter"]=>
array(1) {
["$This"]=>
string(10) "<required>"
}
}
}
["c"]=>
int(3)
}
["b"]=>
int(2)
}
["a"]=>
int(1)
}
int(1)
int(2)
int(3)
string(92) "{"__proto__":{"__proto__":{"__proto__":{"__proto__":null,"toString":{}},"c":3},"b":2},"a":1}"

var_dump するとムダに長くなってます

Object の引数に連想配列をいれることで オブジェクトに変換されます
$a$b の Object のインスタンスの __proto__ プロパティはデフォルトでは $Object_prototype への参照になっています
そして $Object_prototype の先には __proto__ がつながっていません
JavaScript と一緒です

$a → $b → $c → $Object_prototype とチェーンしています
$a->a, $a->b, $a->c では 1, 2, 3 とちゃんと値が取れています

そして $a->toString() としたときには json_encode した文字列が返るようになっています
toString メソッドは $Object_prototype で定義されています
つまり $Object_prototype __proto__ がつながっていると 文字列化できるということです
JavaScript 感に溢れていますね

$This なわけ

JavaScript っぽくこだわった結果 メソッドは捨てました
全部プロパティで無名関数もそこに入れてメソッドを実現します

ただの関数なので $this は存在しないわけです
リフレクションメソッドに $this 束縛できそうなのがありましたが まずメソッドじゃないので無理そう ということであまり調べずに $this を使わない形にしてます
$this をつかわないのですが 見た目としては 第一引数に $this をとりたいです
ですが PHP の仕様上うまくいかないです

毎度わかりづらいPHPの動きですが こうなります
function f($this){
echo $this + 1;
}
f(1);
// 2

function g($this){
echo $this->val + 1;
}
g((object)['val' => 1]);
// Fatal error: Using $this when not in object context

$this を変数名につかってもただ計算するだけなら問題はないですが プロパティアクセスをするとエラーが起きます
「オブジェクト内じゃないのに $this 使ってる」 だそうです
非オブジェクトの変数なら使えて オブジェクトだとエラーという謎挙動
両方ダメか $this にオブジェクトがあればOKにするかにして欲しいものです

$this という変数名でオブジェクトを扱えないのでここの例では $This にしています
ただの変数名なので $self とかでも好きに使えます
メソッドは 第一引数に $this が入ってメソッドが実行されるという制限だけちょっと使いづらいです

まとめ

PHPをJavaScriptっぽくしてみました
意外と簡単にできました
こんなに簡単にできるので他の人で似たことやってる人がいっぱいいそうです




(追記)

prototype の置き場所はグローバルより static 変数のほうがいいかも
こうなります
class Object{
function __call($name, $arguments){
if(is_object($this->__proto__)){
$meth = $this->__proto__->$name;
array_unshift($arguments, $this);
return call_user_func_array($meth, $arguments);
}else{
throw new Exception();
}
}
function __get($name){
if(is_object($this->__proto__)){
return $this->__proto__->$name;
}else{
return null;
}
}

function __construct($arr = null){
if(isset(self::$prototype)){
$this->__proto__ = self::$prototype;
}else{
$this->__proto__ = null;
}
if(is_array($arr)){
foreach($arr as $key => $val){
$this->$key = $val;
}
}
}

static $prototype = null;
}
Object::$prototype = new Object();
Object::$prototype->toString = function($This){
return json_encode($This);
};

サンプルの方です
class Sample extends Object{
function __construct($data){
$this->__proto__ = Sample::$prototype;
$this->data = $data;
}

static $prototype = null;
}
Sample::$prototype = new Object();
Sample::$prototype->a = 200;
Sample::$prototype->double = function($This){
return $This->data * $This->data;
};
Sample::$prototype->plus = function($This, $num){
return $This->data + $num;
};

メンバ変数の初期値には式が書けなくて 定数だけというのはやめてほしい仕様です