PHP

【PHP】マジックメソッドについて理解をまとめる

こんにちは、ともです。

『__』という2つのアンダースコアを先頭にもつメソッド、『マジックメソッド』をご存知でしょうか。

この記事では、そんなPHPのマジックメソッドについて理解をまとめます。

マジックメソッドとは

マジックメソッドとはアンダースコア2つ『 __ 』で始まるメソッドです。

コンストラクタ『__construct()』やデストラクタ『__destruct()』も先頭に2つのアンダースコアを持っていることから、マジックメソッドであることが分かります。

マジックメソッドは基本的に直接呼び出すメソッドではなく、ある状況において実行されるメソッドとなります。

以降では次のマジックメソッドについてまとめます。

  1. __contruct()・__destruct()
  2. __get()・__set()
  3. __isset()・__unset()
  4. __call()・__callStatic()
  5. __sleep()・__wakeup()
  6. __toString()
  7. __invoke()
  8. __clone()

__contruct()・__destruct()

PHPにおいてコンストラタとデストラクタを役割を果たすマジックメソッドになります。

__contruct()インスタンスの生成時に起動するメソッドです

インスタンスの初期化などを行うために利用するため、頻繁に利用するマジックメソッドです。

__destruct()はプロセスの終了やunset()で明示的にインスタンスを破棄した場合に起動するメソッドです。

インスタンス内部に特定のリソース(データベースとの接続情報など)が存在する場合は__destruct()内で破棄する事が可能です。

__get()・__set()

アクセスできないプロパティにアクセスした場合に起動するメソッドを定義する事ができます。

<?php

class Test {
   private $score;
 
   public function __get($name) {
        echo $name . "プロパティは存在しないのにアクセスしました<br>";
    }

    public function __set($name, $value) {
        echo $name . "プロパティは存在しないのに" . $value . "をセットしました<br>";
    }
}

$scoreプロパティを持つTestクラスを用意しました。

これに未定義のプロパティにアクセス、セットしてみます。

$test = new Test();
$test->hoge;
$test->huga = 'aaa';

Testクラスに未定義のhogeプロパティとアクセスし、hugaプロパティに’aaa’をセットしました。

この場合の出力がこちらです。

hogeプロパティは存在しないのにアクセスしました
hugaプロパティは存在しないのにaaaをセットしました

存在しないプロパティにアクセス、セットの処理を行ったことにより、このマジックメソッドが起動している事が分かります。

__isset()・__unset()

アクセスできないプロパティに対して、isset()やunset()を行った際に呼び出されます。

クラスに次のようにマジックメソッドを定義します。

public function __isset($name) {
        echo $name . "プロパティは存在しないのにissetしました<br>";
    }

    public function __unset($name) {
        echo $name . "プロパティは存在しないのにunsetしました<br>";
    }

その後、次のようにisset、unsetを行います。

$test = new Test();
isset($test->hoge);
unset($test->unset);

 

すると、次のように出力される結果となります。

hogeプロパティは存在しないのにissetしました
unsetプロパティは存在しないのにunsetしました

__call()・__callStatic

未定義のインスタンスメソッド/クラスメソッドを呼び出した場合に起動します。

インスタンスメソッド・クラスメソッドの違いについては次の記事にまとめてみましたので参考になれば幸いです。

次のように__call、__callStaticを定義しました。

public function __call($method, $args) {
        echo $method . "インスタンスメソッドが未定義ですが呼び出されました。<br>";
        echo "引数は次のように与えられました。";
        print_r($args);
        echo "<br>";
    }

    public static function __callStatic($method, $args) {
        echo $method . "クラスメソッドが未定義ですが呼び出されました。<br>";
        echo "引数は次のように与えられました。";
        print_r($args);
        echo "<br>";
    }

そして次のようにアクセスしました。

$test->hoge(1, 2, 3);
Test::fuga('a', 'b', 'c');

すると次の結果となりました。

hogeインスタンスメソッドが未定義ですが呼び出されました。
引数は次のように与えられました。Array ( [0] => 1  => 2  => 3 ) 
fugaクラスメソッドが未定義ですが呼び出されました。
引数は次のように与えられました。Array ( [0] => a  => b  => c )

未定義のインスタンスメソッド、クラスメソッドにアクセスしたことにより起動した事が分かります。

__sleep()・__wakeup()

インスタンスをserialize(シリアライズ)、unserialize(アンシリアライズ)する場合に起動するメソッドです。

シリアライズ、アンシリアライズとは

シリアライズ、アンシリアライズを利用するとインスタンスを一時的に保存する場合にとても便利です。

インスタンスを一時的に保存する

データを保存したい場合にDBに格納したり、ファイルに書き出したりすると思います。

ここで、インスタンスをDBやファイルに保存したい場合はどうすれば良いでしょうか

こうすれば、クラス内の変数を一時的に保存しておき、インスタンスを複製したい場合はDBから取ってくれば解決します。

しかし、一時的な保存のために膨大な数の変数を格納するためのテーブルを作成する事は大変です。

そこで、シリアライズ(直列化)が利用できます。

シリアライズすることにより、クラス内部変数を1つの文字列に変換してくれます。

つまり、インスタンスの一時保存のために次の流れを利用する事ができるのです。

シリアライズ化してクラス内部の情報を文字列に変換する事で一次的な保存に役出つ形となりました。

再度生成したい場合は、アンシリアライズすることで再構築する事ができます。

__sleepと__wakeup

__sleepと__wakeupはインスタンスがシリアライズ化されたとき、アンシリアライズ化された時に起動するメソッドです。

次のようにクラスを作成しました。

class Test {
    private $score = 90;
    private $subject = '数学';

    public function __sleep() {
        echo 'シリアライズしました。<br>';
        return array('score', 'subject');
    }

    public function __wakeup() {
        echo 'アンシリアライズしました。<br>';
    }
}

そして次のように、シリアライズ、アンシリアライズしました。

$test = new Test();

$serialized = serialize($test);
$unserialized = unserialize($serialized);

その際の出力は次のようになりました。

シリアライズしました。
アンシリアライズしました。

シリアライズ化する前に、__sleepが実行され、アンシリアライズ化する前に、__wakeupが実行されている事が分かります。

sleep(眠る)、wakeup(起きる)という表現からもシリアライズの一時的に文字列として眠ってもらい、欲しい時に起きてもらう、というニュアンスと合っているのではないでしょか。

__toString()

インスタンスを文字列として扱いたい場合に__toStringが利用できます。

次のように__toString()を追加しました。

  private $subject = '数学';
    public function __toString() {
        return $this->subject . "<br>";
    }

そして次のように、インスタンスを出力してみます。

$test = new Test();
echo $test;

すると出力はこうなります。

数学

インスタンスが文字列として扱う事ができているのが分かります。

__invoke()

__invokeを実装したクラスは、関数のように扱う事が可能になります。

次のようにクラスに__invokeを実装しました。

public function __invoke($params) {
        print_r($params);
        echo 'を受け取りました。<br>';
    }

そしてインスタンスを関数として扱ってみます。

$test = new Test();
// 関数として利用
$test('hoge');
// コールバック関数として利用
array_map($test, ['a', 'b', 'c']);

結果は次のようになりました。

hogeを受け取りました。
aを受け取りました。
bを受け取りました。
cを受け取りました。

関数として利用した場合には、__invoke()が実行されている事が分かります。

__clone()

オブジェクトの複製にはclone文が利用できます。

__clone()はインスタンスがcloneされた場合に起動します。

cloneとは

$test = new Test();
$test1 = $test;
$test1->subject = "国語";

echo "original:" . $test->subject . "<br>";
echo "copy:" . $test1->subject . "<br>";

インスタンスはポインタであるので、代入処理では複製されず、同じインスタンスへのポインタとなります。

そのため『数学』という初期値になって欲しい$test1も出力はどちらも国語になります。

複製はcloneで行う事ができます。

$test = new Test();
$test1 = clone $test;
$test1->subject = "国語";

echo "original:" . $test->subject . "<br>";
echo "cloned:" . $test1->subject . "<br>";

original->数学、cloned→国語、という結果となり別のオブジェクトとして扱えている事が分かります。

__clone()を利用する状況

__clone()を利用する事で、clone時にメソッドを起動する事ができますが、どのような場合に__clone()を利用するのでしょうか。

例えば次のようなクラスを考えます。

<?php

class Test {
    
    public $bar;

}

class Bar {

   public $val;
}

そして次のようにインスタンスを生成します。

$test = new Test();
$test->bar = new Bar();
$test->bar->val = 'bar';

$test1 = clone $test;
echo '=baz前' . "<br>";
echo "original:" . $test->bar->val . "<br>";
echo "cloned:" . $test1->bar->val . "<br>";

$test1->bar->val = 'baz';

echo '=baz後' . "<br>";
echo "original:" . $test->bar->val . "<br>";
echo "cloned:" . $test1->bar->val . "<br>";

上のようにTestクラスの$barに外部クラスBarを注入します。

そして、$test->bar->valへの代入前後で値を確認してみます。

=baz前
original:bar
cloned:bar
=baz後
original:baz
cloned:baz

結果として、$test1のbarにのみ代入したはずが、どちらへも代入されてしまいました。

これはTestクラス内部の変数$barは複製されておらず、ポインタのまま保持されているため、この現象が発生しました。

このように、複製インスタンスが内部にインスタンスを保持している場合、上述のような問題を__clone()内で解決する事ができます。

__clone()の利用

public function __clone() {
        $this->bar = clone $this->bar;
    }

前述の問題を__clone()内でこのように解決できます。

内部に保持していたインスタンスも複製してやる事で解決する事ができます。

まとめ

  1. __contruct()・__destruct()…コンストラクタ・デストラクタ
  2. __get()・__set()…アクセス不可能なプロパティへのゲット・セット
  3. __isset()・__unset()…アクセス不可能なプロパティへのisset・unset
  4. __call()・__callStatic()…アクセス不可能なメソッドへのアクセス
  5. __sleep()・__wakeup()…シリアライズ・アンシリアライズ
  6. __toString()…インスタンスを文字列として扱う場合
  7. __invoke()…インスタンスをメソッドとして扱う場合
  8. __clone()…インスタンスをcloneする場合

マジックメソッドを理解して適切に利用していきたいです。