PHP入門 オブジェクト指向 マジックメソッド

PHPのマジックメソッド(__get/__set/__isset/__unset/__call/__callStatic/__toString/__invoke/__clone/__debuginfo)について解説しています。

マジックメソッドとは

PHPでは、あらかじめ特定の役割を与えられたメソッドとして、マジックメソッドが用意されています。たとえば、__constructや__desctructもマジックメソッドの一種です。マジックメソッドを利用することで、クラスに対して特定の機能を実装出来ます。マジックメソッドは、いずれもシグニチャ(名前と引数、戻り値の組み合わせ)と呼び出しのタイミングが決められているだけで、中身がないメソッドです。必要に応じて、機能そのものは自分で実装する必要があります。

主なマジックメソッド

メソッド呼び出しのタイミング
__get未定義のプロパティを取得しようとしたとき
__set未定義のプロパティを設定しようとしたとき
__isset未定義のプロパティをisset関数で処理しようとしたとき
__unset未定義のプロパティをunset関数で処理しようとしたとき
__call未定後のインスタンスメソッドをコールしたとき
__callStatic未定義の静的メソッドをコールした時
__toStringprint命令などでオブジェクトの文字列表現を要求されたとき
__invokeオブジェクトが関数の形式で呼び出されたとき
__cloneclone命令でオブジェクトを複製したとき
__debuginfovar_dump命令でオブジェクトをダンプしようとしたとき

未定義のプロパティを処理する(__get/__setメソッド)

__get、__setメソッドは、それぞれ未定義のプロパティを取得/設定しようとしたタイミングで呼び出されるメソッドです。__getメソッドは、引数として取得しようとしたプロパティ名を、__setは設定しようとしたプロパティとその値を、それぞれ受け取ります。

構文:__get/__setメソッド

__get(mixed $name):mixed
__set(string $name, mixed $value):void
$nameプロパティ名
$valueプロパティ値

__get/__setメソッドを利用することで、クラスの定義時にあらかじめきておけない任意のプロパティを処理できます。

例:mb_send_mail関数をオブジェクト指向的にラッピングしたMyMailクラス

MyMail.php


<?php
class MyMail {
    // to (%), subject (f), message (X) 705
    public string $to;
    public string $subject;
    public string $message;
    // その他のヘッダー情報を格納するためのプライベート変数 (連想配列) 
    private array $headers = [];
    // 未定義のプロパティを設定すると、 Sheadersにその値をセット
    public function __set(string $name, mixed $value): void { 
        $this->headers[$name] = $value;
    }
    // 未定義のプロパティを取得しようとすると、 $headersから該当する値を取得 
    public function __get(string $name) : mixed {
        return $this->headers[$name];
    }
    // プロパティ情報を基にメールを送信
    public function send(): void {

        $others = '';
        //$headersの内容を基にヘッダー情報 (改行区切り)を生成 
        foreach ($this->headers as $key => $value) { // プロパティ名に含まれる「_」は「-」に変換
            $key = str_replace('', '', $key);
            $others.= "($key): {$value} \n";
        }
        // メールを送信
        mb_send_mail($this->to, $this->subject, $this->message, $others);
    }
}

MyMailクラスでは、任意のメールヘッダーを管理できるようにしています。このようなケースでは、あらかじめ受け取るべきプロパティを定義しておくこともできません。そこで、最低限、必須のプロパティ(to、subject、message)だけを定義しておき、それ以外のプロパティは__set/__getメソッドでは、与えられたプロパティをプライベート変数$headers(連想配列)から出し入れしています。これによって、任意のプロパティを管理できます。

MyMailクラスの利用方法


<?php
require_once 'MyMail.php';
$m = new MyMail();
// 必須のプロパティを設定 
$m->to = 'yamada@example.com'; 
$msubject = 'テストメール';
$m->message='こんにちは、MyMailクラスです。 ';

//任意のプロパティを設定(ハイフンはすべてアンダースコアで指定すること)
$m->From= 'admin@example.com';  //送信元
$mX_Mailer= 'MyMail 10';        // 使用しているメーラー
$m->X_Priority = 1;             // 優先順位
$m->X_MSMail_Priority = 'High'; // 優先順位

// メールを送信
$m-send();
プロパティ名(キー名)に含まれるアンダースコア(_)をハイフン(-)に変換しているのは、プロパティ名にはハイフンを含めることができないためです。そこでプロパティ名にはハイフンの代わりにアンダースコアを使い、クラスの内部で強制的にハイフンに変換しているのです。

未定義のプロパティを処理する(__isset/__unsetメソッド)

__isset、__unsetメソッドは、それぞれ未定義のプロパティをisset関数、unset関数で処理しようとしたタイミングで呼び出されるメソッドです。いずれのメソッドも、引数として処理対象のプロパティ名を受け取ります。

構文:__isset/__unsetメソッド

__isset(string $name):bool
__unset(string $name):void
$nameプロパティ名

__isset/__unsetメソッドは、その性質上、__get/__setメソッドとセットで利用することが多いです。先ほど定義したMyMailクラスに__isset、__unsetメソッドを追加します。

<?php
class MyMail{
・・・中略・・・
//未定義のプロパティをisset関数で判定すると、$headersのキーをチェック
public function __isset(string $name): bool{
  return isset($this->headers[$name]);
}

//未定義のプロパティをunset関数で判定すると、$headersのキーを削除
public function __unset(string $name): void{
  return unset($this->headers[$name]);
}
・・・中略・・・

}

<?php
require_once 'MyMail.php';
$m = new MyMail();
$m->From = 'admin@example.com'; 
var_dump($m->Form);
var_dump(isset($m->From));   //結果:string(17) "admin@example.com"
unset($m->From);             //結果:bool(true)
var_dump($m->Form);          //結果:NULL

ここでは、Fromヘッダー(プロパティ)をisset、unset関数で判定/破棄しています。プロパティ破棄の前後で意図した結果が得られることを確認してください。

未定義のメソッドを処理する(__call/__callStaticメソッド)

__call/__callStaticメソッドは、それぞれ未定義のインスタンスメソッド/静的メソッドをコールしたタイミングで呼び出されるメソッドです。いずれのメソッドも、引数として呼び出されたメソッドの名前と、渡された引数値を受け取ります。

構文:__call/__callStaticメソッド

__call(string $name, array $arguments):mixed
__callStatic(string $name, array $arguments):mixed
$nameメソッド名
$argumentsメソッドに渡された引数

__call/__callStaticメソッドを利用することで、クラス定義の時点では決めておけない任意のメソッドを処理できます。

具体例として、先ほど作成しMyMailクラスにメソッド形式で値を取得/設定できるように__callメソッドを実装します。

MyMail.php

<?php
class MyMail{
   ・・・中略・・・
   //未定義のメソッドが呼び出された場合に実行
   public function __call(string $name, array $args): mixed{
     //引数が渡されなかった場合は、メソッド名に対応するキーの値を取得
     if(count($args) === 0){
       return $this->headers[$name];
       //引数が渡された場合、メソッド名に対応するキーに引数の値を設定(戻り値は設定した値)
     }else{
       return $this->headers[$name] = $args[0];
     }
   }
   ・・・中略・・・
}

これによって、先ほど__get/__setメソッドで実装したのと同等の機能を、__callメソッドで実装しています。ちなみに、第2引数以降が指定された場合、__callメソッドはこれを無視します。

<?php
require_once 'MyMail.php';

$m = new MyMail();
$m->From('admin@example.com');
print $m->From();   //結果:admin@example.com

オブジェクトの文字列表現を規定する(__toString)

既定では、オブジェクトをprintなどの命令で出力することはできません。プロパティ値にアクセスしなくてもオブジェクト変数を指定するだけでオブジェクトの文字列表現を取得できたほうが便利です。そこで登場するのが、__toStringメソッドです。

構文:__stringメソッド

__toString():string

__toStringメソッドは、オブジェクトが文字列に変換された場合の結果を決めるメソッドで、戻り値としてオブジェクトの文字列表現を返す必要があります。

Person.php

<?php
class Person{
    public string $firstName;
    public string $lastName;

    public function __construct(string $firstName, string $lastName){
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    public function show():void{
        1/0;
        print "<p>私の名前は{$this->lastName}{$this->firstName}です</p>";
    }
    public function __toString(): string{
        return $this->lastName.$this->firstName;
    }
}

ここでは、lastNmaeとfirstNameプロパティを連結したものを、__toStringメソッドの戻り値としています。

__toStringメソッドを実装する際は、そのクラスを特徴づける情報(プロパティ)を選別して文字列化するのがポイントです。すべてのプロパティ値を書き出すのが目的ではありません。また、__toStringメソッドで利用したプロパティは、個別のゲッターでも取得できるように配慮します。

オブジェクトを関数として実行する(__invokeメソッド)

__invokeメソッドは、オブジェクトが関数の形式で呼び出された場合に実行されます。

例:FriendListクラスに__invokeメソッドを追加するパターン

__invokeメソッドは、引数としてインデックス番号を受け取り、FriendListクラスの中に登録されたn番目のPersonオブジェクトを返すものとします。

FriendList.php

<?php
class FriendList implements IteratorAggregate {
    //ダミープロパティを定義
    public string $version = '1.0.0';
    public string $name ='友人リスト';
    //Personオブジェクトのリストを格納するためのprivate変数
    private array $list = [];

    //反復処理の対象を定義
    public function getIterator(): Traversable {
        return new ArrayIterator($this->list);
    }
    //Personオブジェクトを追加するためのaddメソッド
    public function add(Person $p): void {
        $this->list[] = $p;
    }
    //指定されたインデックス番号に対応するPersonオブジェクトを取得
    public function __invoke(int $index): mixed{
        return $this->list[$index];
    }

}

__invokeメソッドは、他のマジックメソッドと違って、任意の引数を受け取り、任意の戻り値を返せます。

__invokeメソッドを利用する方法

<?php

require_once 'Person.php';
require_once 'FriendList.php';

//FriendListオブジェクトにPersonオブジェクトをセット
$list =new FriendList();
$list->add(new Person('太郎','山田'));
$list->add(new Person('花子','工藤'));
$list->add(new Person('健司','佐藤'));

//オブジェクトを関数の形式で呼び出し
print $list(1); //結果:工藤花子

オブジェクトを関数形式で呼び出すことで、__invokeメソッドが実行されることを確認できます。__invokeメソッドは、いわゆる「既定のメソッド」を実装する際に利用します。

オブジェクトの複製方法をカスタイマイズする(__cloneメソッド)

clone命令はオブジェクトのコピーを生成するための命令です。ただし、clone命令の既定の挙動はシャローコピーです。つまり、コピー対象のプロパティ値に参照(その多くはオブジェクト)が含まれていた場合には、参照をそのままコピーするということです。しかし、オブジェクトの内容をディープコピーしたいということもあるでしょう。ディープコピーとは、プロパティ値にオブジェクト(参照)が含まれていた場合、参照先のオブジェクトもコピーされます。__cloneメソッドは、clone命令による複製が完了したタイミングで呼び出されるメソッドです。__cloneメソッドを利用することで、コピー先のプロパティ値を強制的に変更できます。

<?php
class FriendList implements IteratorAggregate {
    //ダミープロパティを定義
    public string $version = '1.0.0';
    public string $name ='友人リスト';
    //Personオブジェクトのリストを格納するためのprivate変数
    private array $list = [];

    //反復処理の対象を定義
    public function getIterator(): Traversable {
        return new ArrayIterator($this->list);
    }
    //Personオブジェクトを追加するためのaddメソッド
    public function add(Person $p): void {
        $this->list[] = $p;
    }

    public function __clone(): void{
        foreach ($this->list as $value){
            $value = clone $value;
        }
    }

}

ここではプライベート変数$listの内容をforeach命令で取り出し、複製を生成した上で書き戻しています。foreach命令でもともとの配列に値を書き戻すには「&$value」のように値を参照渡しする必要があります。このほか、__cloneメソッドは特定のプロパティを複製の対象外としたいときにも利用できます。たとえば、listプロパティを複製したくない場合は次のように書きます。

public function __clone(): void{
  $this->list=[];
}

不変クラス

不変クラスとは、オブジェクトを最初に生成したところから、一切の値(プロパティ)が変化しないクラスのことです。オブジェクトの状態が意図せず変えられてしまう心配がないことから、いわゆる「可変クラス」よりも利用が簡単になり、結果として、バグの混入が防げます。


<?php
require_once 'Person.php';
final class Book {

    private string $title;   //➊
    private Person $author;  //➊
    
    public function __construct(string $title, Person $author) {   //➋
        $this->title= $title;
        $this->author = clone $author;//➎
    }

    public function getTitle(): string {
        return $this->title;
    }

    // オブジェクトを返すゲッター
    public function getAuthor(): Person {
        return clone $this->author;   //➍
    }

    // title メソッドだけを変更した複製を生成
    public function withTitle (string $title): self{//➐
        $b = clone $this;
        $b->title = $title;
        return $b;
    }

    // プロパティの追加を禁止
    public function __set(string $name, mixed $value): void {//➌
        throw new Exception('Unknown property.');
    }

    public function __clone(): void {
        $this->author = clone $this->author;//➏
    }
}

❶❷すべてのプロパティはprivate宣言

不変クラスでは、すべてのプロパティはprivateとして宣言します(❶)。そして、これらのprivateプロパティを初期化するのは、原則としてコンストラクターだけの役割です(❷)。不変クラスなので、セッターを設けてはいけません

❸プロパティの追加を禁止

PHPでは個々のインスタンスに対してプロパティを追加できます。しかし、不変クラスでは当然独自のプロパティを許容すべきではありません。未宣言のプロパティに代入しようとした場合に、__setメソッドで明示的に例外を発生します。

❹オブジェクトを返すゲッターには注意

ただし、オブジェクトを返すゲッターには要注意です。オブジェクトの代入は参照の引き渡しです。よって、ゲッター経由で渡されたオブジェクトを変更してしまえば、その内容はもとのプロパティにも影響します。このようなオブジェクトを渡す際に、clone命令で複製を生成します。このような手法を防衛的コピーと言います。

❺❻コンストラクター/__cloneメソッドでも防衛的コピー

コンストラクター/__cloneメソッドでも、プロパティでオブジェクトを扱っている場合には同じです。代入元の変更が代入/コピー先にも影響してしまうので、受け渡しに際しては複製したものを利用します。

❼プロパティの変更はWithXxxxxメソッドで

不変クラスでも、特定のプロパティだけを変更したいという場合がでてきます。そのような場合には、withXxxxxのようなメソッドを用意します。WithXxxxxメソッドでも、現在のオブジェクトは変更しません。代わりに、複製を生成した上で、そのプロパティを変更したものを返すわけです

デバック情報をカスタイマイズする(__debuginfoメソッド)

var_dump関数を利用することで、変数の内容を型/値など見やすい形式でダンプできます。__debugInfoメソッドは、オブジェクトをこのvar_dump関数でダンプしようとしたタイミングで呼び出されるメソッドです。

var_dump関数は、既定ですべてのpublic/protected/privateプロパティを出力しますが、__debugInfoメソッドを利用することで一部のプロパティだけを出力対象としたり、名前/値をカスタイマイズできます。

構文:__debugInfoメソッド

__debugInfo():array

例:Personクラスのプロパティを「日本語名=>値」の形式にする

__debugInfoメソッドを利用では、出力すべき情報を「名前 => 値」の形式で返すようにします。

<?php
class Person{
    public string $firstName;
    public string $lastName;

    public function __construct(string $firstName, string $lastName){
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    public function show():void{
        1/0;
        print "<p>私の名前は{$this->lastName}{$this->firstName}です</p>";
    }
    public function __debugInfo(): array{
        return [
          '名' => $this->firstName,
          '性' => $this->lastName
        ];
    }
}

利用方法


<?php
require_once 'Person.php';
$p = new Person('小次郎','田中');
var_dump($p);
実行結果
実行結果