このコーナーについて
私は根っからの文系なのでプログラミングや情報技術についてはITパスポート程度の知識しかなく、現在は全くの独学でコードや言語等のスキルの勉強はチュートリアル等でコードを書いて成果物を作りながら学んでいけるが、それらの過程で出てくる用語や技術についてはイマイチ学びきれない。
なので、折に触れて書籍及びQlita記事等のネットサーフィンで学んだ座学的知識をアウトプットすることがこのコーナーの目的である。
今回
パーフェクトPHPによるオーバーロードとそのマジックメソッドについての学習履歴
本題
<?php
class SomeClass {
private $values = array(); // privateな値を保存するコンテナ
// privateなコンテナへのアクセサ(getter)
public function __get($name) {
echo "get: $name",PHP_EOL;
if (!isset($this->values[$name])) {
throw new OutOfBoundsException($name . "not found.");
}
return $this->values[$name];
}
// privateなコンテナへのアクセサ(setter)
public function __set($name,$value) {
echo "set: $name setted to $value",PHP_EOL;
$this->values[$name] = $value;
}
public function __isset($name) {
echo "isset: $name", PHP_EOL;
return isset($this->values[$name]);
}
public function __unset($name) {
echo "unset: $name", PHP_EOL;
unset($this->values[$name]);
}
public function __call($name,$args) {
echo "call: $name", PHP_EOL;
// アンダースコアをつけ、メソッド名とする
$method_name = "_" . $name;
if(!is_callable(array($this,$method_name))) {
throw new BadMethodCallException($name . "method not found.");
}
return call_user_func_array(array($this,$method_name),$args);
}
public static function __callStatic($name,$args) {
echo "callStatic: $name", PHP_EOL;
$method_name = "_" . $name;
if(!is_callable(array("self",$method_name))) {
throw new BadMethodCallException($name . "method not found.");
}
return call_user_func_array(array("self",$method_name),$args);
}
private function _bar($value) {
echo "bar called with arg '$value'", PHP_EOL;
}
private static function __staticBar($value) {
echo "staticBar called with arg '$value'", PHP_EOL;
}
}
// 以下テストケース
$test = new SomeClass();
$test->foo =10;
var_dump($test->foo);
var_dump(isset($test->foo));
var_dump(empty($test->foo));
unset($test->foo);
var_dump(isset($test->foo));
$test->bar('baz');
SomeClass::staticBar('baz');
オーバーロードとは特定の処理がクラスまたはオブジェクトに施されたときのデフォルトの挙動を上書きする機能である。
そのために用いられるマジックメソッドは一様にあるクラスやオブジェクトのアクセス不能なプロパティ、及びメソッドにアクセスして動作をする。
よってオーバーロードとは以上のような場面で通常エラーが発生する挙動を上書きし、定義した処理を実行させる機能と言ってもいい。
追記:2019/9/30
以上の説明がオーバライドと混同しているのでは? とコメントでご指摘頂いたので補足。
この書き方の根拠としては
パーフェクトPHPのp.141における
オーバーロードは、特定の処理がクラスまたはオブジェクトに施されたときのデフォルトの挙動を上書きする機能です。クラスやオブジェクトのアクセス不能なプロパティやメソッドを参照されたとき、通常エラーが発生する挙動を6つのマジックメソッドを用いてオーバーロードすることができます。
という解説です。
オーバーライドは継承に際して、親元から引き継いだメソッドの処理を上書きして子クラスに実装する行為でいわばメソッドの上書きという意味合いが強いのに対して、オーバーロードにおける上書きは本来ならエラーという事実を上書きするという意味合いが強くその実は全く違うものだと私は認識しています。
あとにやる、例外処理の考え方に近いかもしれません。
そもそも、PHPのオーバーロードは本来の意味でのオーバーロードとは異なる(そもそも、PHPにそれが実装されていない)というのが根本の問題であるのはコメントでご指摘頂いてるとおりです。
コメントでご指摘いただいている通り、ドキュメントではオーバーロードにおける上書きのことを「動的に作成する」、つまり
上書きするのではなく、未定義のプロパティ、メソッドにアクセスされた時に動的に処理を作成しています
と表現している。
パーフェクトPHPのそれとどちらが最初の認識としてわかりやすいかは人によりけりなので正解はどちらともいえません。
私は動的に処理を作成するというフレーズにイマイチピンとはこないなと思い、パーフェクトPHPの解説に従いました。
ですが、オーバーライドとオーバーロードはまるで別物であるので必ず混同することのないようにしっかり役割を理解しておきましょう。
閑話休題。
マジックメソッドの動作を確認しつつ、具体的に処理を確認していく。
class SomeClass {
private $values = array(); // privateな値を保存するコンテナ
public function __get($name) {
echo "get: $name",PHP_EOL;
if (!isset($this->values[$name])) {
throw new OutOfBoundsException($name . "not found.");
}
return $this->values[$name];
}
public function __set($name,$value) {
echo "set: $name setted to $value",PHP_EOL;
$this->values[$name] = $value;
}
アクセス不能なプロパティに対して、getは引数に取得しようとするプロパティ名を指定しそのプロパティを取得、setは逆に代入しようとしているプロパティ名と代入しようとした値を引数に指定し、そのプロパティに値を代入するためのメソッドである。
実際に実行してみる。
$test = new SomeClass();// インスタンス生成
$test->foo = 10; // 存在しないプロパティfooを呼び出し、10を代入する
set:foo setted to 10 //setメソッドが働き、foo = 10が代入される。
この時、foo->10というプロパティが成立するように見えるが
public function __set($name,$value) {
echo "set: $name setted to $value",PHP_EOL;
$this->values[$name] = $value;
}
と定義されているので、$fooをキー、10を値としてプロパティは成立せず、値だけprivateコンテナに代入する形になる。
set実行の処理に引き続いて、以下のgetを実行しても
var_dump($test->foo);
get: foo
int(10)
fooプロパティは作成されていない = アクセス不能なプロパティの取得を実行している。
という挙動が発生するため、getメソッドが呼び出されることになり、以上のような結果となる。
引き続いて2つのメソッドを実行する。
```php:issetメソッドとunsetメソッド
public function __isset($name) {
echo "isset: $name", PHP_EOL;
return isset($this->values[$name]);
}
public function __unset($name) {
echo "unset: $name", PHP_EOL;
unset($this->values[$name]);
}
どちらのメソッドもアクセス不能なプロパティに対してisset()・unseet()が実行されたときに起動し、それを実行するメソッドである。
つまり
```php:__issetメソッド実行と結果
var_dump(isset($test->foo));
var_dump(empty($test->foo));
isset: foo // fooという文字列が引数に渡され、メソッド実行
bool(true) // 先にsetメソッドで値がセットされているのでtrue
isset: foo // これ以下empty()の処理。下記参照
get: foo
bool(false) // setメソッドで値がセットされているのでfalse
empty()は処理的には変数がセットされているか、次にその変数が空でないかを検証し、値を返す仕組みなのでissetメソッドとgetメソッドが呼び出されることになる。
故に上のような結果となる。
unset($test->foo);
var_dump(isset($test->foo)); // unsetされたかどうかをissetで検証する。
unset: foo
isset: foo // 以下unsetされたかどうかの検証結果
bool(false)
先々のメソッドと同じく、存在しないプロパティであるfooに対してunsetが働いたのでメソッドが起動し、上のような結果となる。
public function __call($name,$args) {
echo "call: $name", PHP_EOL;
// アンダースコアをつけ、メソッド名とする
$method_name = "_" . $name;
if(!is_callable(array($this,$method_name))) {
throw new BadMethodCallException($name . "method not found.");
} // 未定義のメソッドをコールバックが参照したり、引数を指定しなかったりした場合に実行される例外処理。
return call_user_func_array(array($this,$method_name),$args);
} // 第2引数でパラメータの配列を指定して、それを第1引数に指定した関数をコールする処理。($this,$method_name)がコールする関数を表している。$thisを使うことでiインスタンスのプロパティ・メソッドにアクセスできるようになっている。
public static function __callStatic($name,$args) {
echo "callStatic: $name", PHP_EOL;
$method_name = "_" . $name;
if(!is_callable(array("self",$method_name))) {
throw new BadMethodCallException($name . "method not found.");
}
return call_user_func_array(array("self",$method_name),$args);
}
private function _bar($value) {
echo "bar called with arg '$value'", PHP_EOL;
}
private static function _staticBar($value) {
echo "staticBar called with arg '$value'", PHP_EOL;
}
callメソッドとcallStaticメソッドはどちらもアクセス不能なメソッドが呼び出されるような処理が行われた場合に実行される。
callStaticメソッドはstatic(静的に)呼び出された場合に実行されることになる。
さて、以下が実行結果である。
$test->bar('baz'); // メソッドの呼び出し
$SomeClass::staticBar('baz');
call: bar
bar called with arg 'baz'
callStatic: staticBar
staticBar called with arg 'baz'
中段2つがcallメソッドの処理結果。
まず、barメソッドがbazという文字列を引数に呼び出される。
が、barメソッドは存在しないのでアクセス不能であるから、callメソッドが起動する。
これによりまず、$nameがbarとなり、メソッド名が_barとなる。
さらに、call_user_func_arrayでここで決めたメソッド名に引数'baz'を渡してコールバックするという処理になる。
該当する_barメソッドはprivateになっていて外部からはアクセス不能なのでエラーになるところを、callメソッドでcall_user_func_arrayでコールされると上書きしているということになる。
よって_barメソッドが実行されるということになる。
callStaticメソッドも同じ理屈であるが、call_user_func_arrayで関数を指定するときにインスタンスの指定のところをselfにしなければならないこと。
これはstatic(静的)なクラスにアクセスするからである。