なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

PHPで始めるプロトタイプベースOOP (前編)

前作: Pythonで始めるプロトタイプベースOOP
opaupafz2.hatenablog.com
opaupafz2.hatenablog.com

PHPのクラスはオブジェクトではなく、一種の型(ユーザー定義型)という扱いであり、クラスがオブジェクトであるクラスベース、もしくはプロトタイプベースのオブジェクト指向型言語とは違って、プロトタイプベース的なプログラミング手法がやりにくい・・・はずだったのだが、実はPHPではある機能を使うことで、割と簡単に、かつ簡潔なプロトタイプベースのオブジェクト指向プログラミング(以下OOP)をすることが可能である。

PHPの環境

PHP 5.6以降とする。

マジックメソッド

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に渡されている。$namestring型である。
また、__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にメソッドの引数が渡されている。$namestring型で、$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()関数が適用されたことがわかる。

メソッドスロットの定義

後編へ
opaupafz2.hatenablog.com

*1:PHPにおけるインスタンス変数。C#やKotlinのプロパティではない

*2:PHP8.0以前のユーザーにとっては聞き慣れない言葉かもしれないがmixed型というのは何でもありな型である

*3:PHPの配列は静的ではなく、動的である。そのため、自由自在に要素を追加したり削除したりすることができる