前作: Pythonで始めるプロトタイプベースOOP
opaupafz2.hatenablog.com
opaupafz2.hatenablog.com
PHPのクラスはオブジェクトではなく、一種の型(ユーザー定義型)という扱いであり、クラスがオブジェクトであるクラスベース、もしくはプロトタイプベースのオブジェクト指向型言語とは違って、プロトタイプベース的なプログラミング手法がやりにくい・・・はずだったのだが、実はPHPではある機能を使うことで、割と簡単に、かつ簡潔なプロトタイプベースのオブジェクト指向プログラミング(以下OOP)をすることが可能である。
マジックメソッド
PHPのクラスには、マジックメソッドというオブジェクトのある特定の動作に対して自分で処理を定義するためのメソッドを定義することができる。
今回使うマジックメソッドは__get()
/__set()
, __call()
, __isset()
, __unset()
の4つである(ちなみに実はコンストラクタの定義に使われる__constructor()
もマジックメソッドなのだが今回は使わない)。
__get()
__get()
は存在しない、あるいはprivate
, protected
なプロパティ*1を参照しようとすると呼ばれるマジックメソッドである。
<?php class A { // __get()マジックメソッド public function __get($name) { echo get_class($this), "::\$${name}は存在しないプロパティです。", PHP_EOL; } } $a = new A(); $a->does_not_exist; // 存在しないプロパティを参照 ?>
以上のコードを実行すると、以下のようになる。
A::$does_not_existは存在しないプロパティです。
プロパティ名does_not_exist
が__get()
の引数$name
に渡されている。$name
はstring
型である。
また、__get()
はプロパティが存在しなかった代わりの値を返すこともできる。返す値の型は様々な値を返しうるのでmixed
型*2である。
<?php // Pikachuクラス class Pikachu { // タイプは"でんきタイプ" public $type = "でんきタイプ"; public function __get($name) { // "typo"とタイポしたら代わりに"type"が呼び出されます if ($name === "typo") { return $this->type; } // mixed型なのでvoid型の値を返しても良い } } $pikachu = new Pikachu(); echo $pikachu->typo, PHP_EOL; // あっ間違ってtypoを参照しちゃった! ?>
以上のコードを実行すると以下のようになる。
でんきタイプ
以上の結果から、存在しない$typo
プロパティを参照しようとしたら$type
プロパティが参照されていることがわかる。
__set()
__set()
は__get()
の反対、つまり存在しない、あるいはprivate
, protected
なプロパティに代入しようとすると呼ばれるマジックメソッドである。
<?php class A { // __set()マジックメソッド public function __set($name, $value) { echo get_class($this), "::\$${name}に${value}を代入できません。", PHP_EOL; } } $a = new A(); $a->does_not_exist = "unknown value"; // 存在しないプロパティに代入 ?>
以上のコードを実行すると、以下のようになる。
A::$does_not_existにunknown valueを代入できません。
__set()
の第一引数$name
にプロパティ名does_not_exist
が、第二引数$value
に"unknown value"
が渡されている。$name
は__get()
と同じでstring
型だが、$value
は様々な値が代入されうるのでmixed
型である。
__set()
は存在しないプロパティへの代入に対する動作なので、返す値の型はvoid
型である。無論、プロパティが存在しなかった場合には別のプロパティに代入させることもできる。
<?php // Ninetalesクラス class Ninetales { // タイプは"ほのおタイプ" public $type = "ほのおタイプ"; public function __set($name, $value) { // "typo"とタイポしたら代わりに"type"に代入されます if ($name === "typo") { $this->type = $value; } } } $ninetalesA = new Ninetales(); // あっ間違ってtypoに代入しちゃった! $ninetalesA->typo = "こおり・フェアリータイプ"; echo $ninetalesA->type, PHP_EOL; ?>
以上のコードを実行すると以下のようになる。
こおり・フェアリータイプ
以上の結果から、存在しない$typo
プロパティに代入しようとしたら$type
プロパティに代入されていることがわかる。
__call()
__call()
は存在しない、あるいはprivate
, protected
なメソッドを実行しようとすると呼ばれるマジックメソッドである。
つまるところ__get()
のメソッド版である。
<?php class A { // __call()マジックメソッド public function __call($name, $arguments) { echo get_class($this), "::${name}()は存在しないメソッドです。", PHP_EOL; echo "引数: ", implode(", ", $arguments), PHP_EOL; } } $a = new A(); $a->does_not_exist(36, "普通だな!"); // 存在しないメソッドを実行 ?>
以上のコードを実行すると、以下のようになる。
A::does_not_exist()は存在しないメソッドです。 引数: 36, 普通だな!
__call()
の第一引数$name
にメソッド名does_not_exist
が、第二引数$arguments
にメソッドの引数が渡されている。$name
はstring
型で、$arguments
は引数が複数であることもあるためarray
型である。
__call()
は__get()
と同じくメソッドが様々な値を返しうるので、mixed
型である。
$arguments
を他の関数などに適用する場合、以下のようにする。
<?php other_func(...$arguments); ?>
__isset()
__isset()
は存在しない、あるいはprivate
, protected
なプロパティをisset()
関数, empty()
関数に適用してfalse
が返されると呼ばれるマジックメソッドである。
<?php class A { // __isset()マジックメソッド public function __isset($name) { echo get_class($this), "::\$${name}は存在しないプロパティです。", PHP_EOL; return false; // 必ずbool型の値を返す } } $a = new A(); // 存在しないプロパティをisset()に適用 echo isset($a->does_not_exist) ? "true" : "false", PHP_EOL; ?>
A::$does_not_existは存在しないプロパティです。 false
__isset()
の戻り値はbool
型なので必ずbool
型の値を返さなければならない。
__unset()
__unset()
は存在しない、あるいはprivate
, protected
なプロパティをunset()
関数に適用したときに呼ばれるマジックメソッドである。
<?php class A { // __unset()マジックメソッド public function __unset($name) { echo get_class($this), "::\$${name}は存在しないプロパティです。", PHP_EOL; // 必ずvoid型の値を返す } } $a = new A(); unset($a->does_not_exist); // 存在しないプロパティをunset()に適用 ?>
A::$does_not_existは存在しないプロパティです。
__unset()
の戻り値はvoid
型なので必ずvoid
型の値を返さなければならない。
PHPでプロトタイプベースOOPを実践
では以上のマジックメソッドの知識をもとに、PHPでプロトタイプベースOOPを実践してみる。
スロットの定義
まず、スロットの定義を$obj->prop
の形で実現したい。
<?php $obj->prop = "PHP"; // この時点では$propは存在しない echo "Hello, ", $obj->prop; ?>
これ自体は__get()
/__set()
があれば実現できそうだが、クラスそのものにプロパティを動的に追加することはPHPでは不可能である。
しかし、__get()
/__set()
の第一引数はstring
型である。であれば、array
型を使って連想配列を作ればスロットと同等の機能が実現できそうである*3。
それではそれを考慮したうえで、プロトタイプベースOOPの基本部分の実装を行う。
<?php final class ParentObject { private $slot = []; public function __get($name) { if (isset($this->slot[$name])) { // スロットがあった場合、それを返す return $this->slot[$name]; } else { // スロットがなかった場合、例外を投げる throw new Error("Undefined slot '${name}'"); } } public function __set($name, $value) { // スロットの定義を行う $this->slot[$name] = $value; } } ?>
では実際に使ってみよう。
<?php require_once "parentobject.php"; $obj = new ParentObject(); // ペアレントを生成 $obj->name = "PHP"; // nameスロットに"PHP"を定義 echo "Hello, $obj->name.", PHP_EOL; // "Hello, PHP."と表示 $obj->does_not_exist; // 存在しないスロットを参照する ?>
Hello, PHP. PHP Fatal error: Uncaught Error: Undefined slot 'does_not_exist' in ...(省略)
以上からオブジェクトにスロットを定義し、定義したスロットは参照することができた。さらに、存在しないスロットを参照しようとすると例外を投げることも確認できた。
スロットの存在の確認と削除
しかし、お気づきの方もいるかもしれないが、このままではisset()
関数とunset()
関数に対してプロパティを適用できない。なぜならスロットの正体はarray
型のプロパティ$slot
だからである。つまり、isset()
関数, unset()
関数に適用しなければならないのはParentObject::$slot[<スロット>]
である。
その解決法として、__isset()
と__unset()
を使う。先ほどのparentobject.php
にそれを付け加えてみる。
<?php final class ParentObject { private $slot = []; public function __get($name) { if (isset($this->slot[$name])) { // スロットがあった場合、それを返す return $this->slot[$name]; } else { // スロットがなかった場合、例外を投げる throw new Error("Undefined slot '${name}'"); } } public function __set($name, $value) { // スロットの定義を行う $this->slot[$name] = $value; } public function __isset($name) { // 代わりに$this->slot[$name]を適用した結果を返す return isset($this->slot[$name]); } public function __unset($name) { // 代わりに$this->slot[$name]を適用 unset($this->slot[$name]); } } ?>
それでは使ってみよう。
<?php require_once "parentobject.php"; $obj = new ParentObject(); // ペアレントを生成 $obj->name = "PHP"; // スロットを定義 echo isset($obj->name) ? "true" : "false", PHP_EOL; // isset()にスロットを適用 unset($obj->name); // unset()にスロットを適用 echo isset($obj->name) ? "true" : "false", PHP_EOL; ?>
true false
実行結果から、スロットに対してisset()
関数, unset()
関数が適用されたことがわかる。