PHPのマジックメソッド__set(), __get()解説。実用的なクラスを考えてみる

PHPマジックメソッド__set(), __get() 解説 PHP
※当サイトはアフィリエイト広告を掲載しています。

PHPにはマジックメソッドという機能があります。オブジェクトに対し、特定の動作を行った際に動作するメソッドのことです。

その中でも__set(), __get()というメソッドは比較的よく知られているのではないでしょうか。

しかしながら、

  • 今一使い所が分からない。
  • 文法は知っていても実践で活かせない。
  • 結局使ってない……。

という方も多いのではないでしょうか。

実際のところ__set(), __get()を使わなくても、普通にWebアプリケーションを作ることはできます。なので使わなくても問題はありません。

しかしながら、使ってみることで新たな可能性に繋がることもあるでしょう。……ということで、私自身の勉強も兼ねて__set(), __get()の解説および実用的なアイデアを考えてみました。

あなたのプログラミングの参考にしていただければ幸いです。

__set(), __get()の概要

まずは__set(), __get()を全くご存じ無い方向けに、概要をご紹介します。

すでにご存じの方は次項に飛ばしてください。

__set() を使った例

__set() はPHPのマジックメソッドの一つで、クラスの外部からアクセス不可能なプロパティに値を設定しようとした際に、自動的に呼び出されます。

アクセス不可能なプロパティとは、 protectedprivate プロパティだったり、未定義のプロパティだったりです。これは後述する__get()においても同じですので覚えておいてください。

__set() は2つの引数を取ります。1つ目は設定しようとしたプロパティの名前、2つ目は設定しようとした値です。

以下が簡単な例です。

class SampleClass {
    private $hiddenProperty;

    public function __set($property, $value) {
        if ($property === "hiddenProperty") {
            // この例では"Hello, World!"を代入
            $this->hiddenProperty = $value;
        }
    }
}

$obj = new SampleClass();
$obj->hiddenProperty = "Hello, World!";  // __set() メソッドが呼び出され値をセット

この例では、hiddenProperty という private プロパティに直接アクセスしようとすると、__set() が自動的に呼び出され、指定された値が hiddenProperty に代入されます。

__set() を活用することで、プロパティへの値の代入をカスタマイズしたり、特定の条件下でのみ値を代入したりといった実装が可能です。

__get() を使った例

__get() は、クラスの外部からアクセス不可能なプロパティの値を取得しようとした際に自動的に呼び出されます。

本メソッドは、取得しようとしたプロパティの名前を唯一の引数として受け取ります。

class SampleClass {
    private $hiddenProperty = "secret value";

    public function __get($property) {
        if ($property === "hiddenProperty") {
            return $this->hiddenProperty;
        }
    }
}

$obj = new SampleClass();
echo $obj->hiddenProperty;  // __get() メソッドが呼び出され、"secret value" と表示される

この例では、hiddenProperty という private プロパティの値にアクセスしようとすると、__get() メソッドが自動的に呼び出され、その結果として hiddenProperty の値が返されます。

__get() を利用することで、プロパティの読み取り動作をカスタマイズしたり、読み取り時のバリデーションや加工などのロジックを導入することができます。

組み合わせて使用する

__set()と__get()は単品でも使うことは可能ですが、組み合わせて使用することでより複雑な機能を実装することが可能です。

いきなり難しいのを挙げても分からなくなりますから、先ほどの__set(), __get()を組み合わせてみましょう。

class SampleClass {
    private $hiddenProperty = "secret value";

    public function __get($property) {
        if ($property === "hiddenProperty") {
            return $this->hiddenProperty;
        }
    }

    public function __set($property, $value) {
        if ($property === "hiddenProperty") {
            $this->hiddenProperty = $value;
        }
    }
}

$obj = new SampleClass();
echo $obj->hiddenProperty;  // __get() メソッドが呼び出され、"secret value" と表示される

$obj->hiddenProperty = "Hello, World!";  // __set() メソッドが呼び出される
echo $obj->hiddenProperty;  // "Hello, World!" と表示される
Lara
Lara

要は値をセット・ゲットする際に呼び出されるだけ。それを定義するだけなので、文法自体は特別難しいわけではありませんね。

__set(), __get()の使い所

基本的な文法が分かったところで、それを実務で使うことはなかなか難しいです。

使うとどう良いのか。どんなメリットがあるのかなど、コード例を通じて「使い所」を考えてみたいと思います。

プロパティの管理を個別に行う例

プロパティは通常、private $nameなどのように、あらかじめ登録して使用します。ただ、必要に応じてチコチコとプロパティを定義するのは地味に面倒です。

それに加えて、値をセットするsetterや値をゲットとするgetterも都度書いていたらもっと面倒。以下がそういったごく普通のコード例です。

class User {
    private $name;
    private $age;

    // 名前のgetter
    public function getName() {
        return $this->name;
    }

    // 名前のsetter
    public function setName($name) {
        if (empty($name)) {
            throw new Exception("名前は空ではいけません。");
        }
        $this->name = $name;
    }

    // 年齢のgetter
    public function getAge() {
        return $this->age;
    }

    // 年齢のsetter
    public function setAge($age) {
        if ($age < 0 || $age > 150) {
            throw new Exception("年齢が不正な値です。");
        }
        $this->age = $age;
    }
}

$user = new User();
$user->setName("Taro");
$user->setAge(25);

echo $user->getName(); // Taro
echo $user->getAge();  // 25

これでも問題は無いのですが、プロパティが増える度にコード量がどんどん増加します。

__set(), __get()で動的なプロパティの管理をした例

__set(), __get()を使えば、プロパティを動的に生成・取得することが容易になります。

class User {
    private $data = [];

    public function __set($property, $value) {
        switch ($property) {
            case 'name':
                if (empty($value)) {
                    throw new Exception("名前は空ではいけません。");
                }
                $this->data['name'] = $value;
                break;
            case 'age':
                if ($value < 0 || $value > 150) {
                    throw new Exception("年齢が不正な値です。");
                }
                $this->data['age'] = $value;
                break;
            default:
                throw new Exception("{$property} は不明なプロパティです。");
        }
    }

    public function __get($property) {
        if (array_key_exists($property, $this->data)) {
            return $this->data[$property];
        }
        throw new Exception("{$property} プロパティは存在しません。");
    }
}

$user = new User();
$user->name = "Taro";
$user->age = 25;

echo $user->name;  // Taro
echo $user->age;   // 25

これならば、プロパティが増えても手間が少ないです。

プロパティへのアクセスや代入の挙動を一箇所で制御・管理することができる。つまりは、コードの簡潔さを保ちつつ、代入時の特別な挙動やバリデーションを導入することも可能になります。

注意点

__set()__get() は便利なマジックメソッドですが、使い方には注意が必要です。

あらかじめプロパティとして定義されていないとういうことは、どのプロパティが存在し、どのような振る舞いをするのかを把握するのが難しくなるケースもあります。

また、あらかじめ定義されていない=そのままではエディタのコードヒントもききません。

きちんとした設計の上で__set()__get() は使う必要がありそうです。

設定値を管理するクラス

使い方や使い所は分かっても、まだなんとなく……という感じな方も多いと思います。そこで、ここからは__get(), __set()を使ったより実用的なクラスを考えてみたいと思います。

まずは設定値を管理するクラスを考えてみました。

概要

__get()__set() を使って設定値を管理するシンプルな設定クラスの例です。

このクラスでは、設定値のバリデーションや変更時のコールバック、デフォルト値の提供などの機能を持っています。

コード

少し長いですが、それほど複雑なことをしているわけではありません。多めにコメントを入れているので読んでみてください。

class SettingsManager {
    private $settings = [];

    // デフォルトの値を定義
    private $defaultSettings = [
        'theme' => 'light',
        'language' => 'en',
    ];

    // この中からしか選択できないようにする
    private $validators = [
        'theme' => ['light', 'dark'],
        'language' => ['en', 'jp', 'fr', 'de'],
    ];

    // コールバック関数の保持用
    private $onChangeCallbacks = [];

    // ゲット ※後述します
    public function __get($name) {  
        return $this->settings[$name] ?? $this->defaultSettings[$name] ?? null;
    }

    // セット&バリデーション
    public function __set($name, $value) {
        if (isset($this->validators[$name]) && !in_array($value, $this->validators[$name])) {
            throw new InvalidArgumentException("Invalid value for setting {$name}");
        }
        $this->settings[$name] = $value;
        $this->triggerOnChange($name, $value);
    }

    // コールバック関数を登録するメソッド
    public function registerOnChange($name, callable $callback) {
        $this->onChangeCallbacks[$name][] = $callback;
    }

    // 値に変更があった時に呼ばれるメソッド
    private function triggerOnChange($name, $value) {
        if (!isset($this->onChangeCallbacks[$name])) {
            return;
        }
        foreach ($this->onChangeCallbacks[$name] as $callback) {
            $callback($value);
        }
    }
}

// 使用例
$settings = new SettingsManager();

// 変更時のコールバック関数を登録
$settings->registerOnChange('theme', function ($value) {
    echo "Theme changed to: {$value}\n";
});

$settings->theme = 'dark';  // Theme changed to: dark
echo $settings->theme;  // dark

改めて、この SettingsManager クラスでは、以下のようなことができます。

  1. オブジェクトのプロパティのように設定値を取得/設定する。
  2. 特定の設定の変更を監視するコールバック関数を登録する。
  3. 設定値のバリデーションを行う。

コメントと併せてご覧いただくことで、どの部分が対応しているか分かると思います。

補足

もしかすると、以下の部分で「?」になった方もいるかもしれません。

    public function __get($name) {  
        return $this->settings[$name] ?? $this->defaultSettings[$name] ?? null;
    }

これは ?? 演算子、正確には「null合体演算子(Null Coalescing Operator)」というもので、PHP 7.0 で導入されました。

?? 演算子は、第一オペランドが非 null の値であればそれを返し、 そうでない場合は第二オペランドを返します。要はisset()をより簡単に書けるようにしたものですね。

つまり今回のコードは、以下を順番に判定していることになります。

  1. $this->settings[$name] がnullでなければそれをreturn
  2. $this->defaultSettings[$name]がnullでなければそれをreturn
  3. ここまで全てnullなら、nullをreturn

詳しく知りたい方は、以下ページもご覧になってみてください。

データバインダークラス

最後に、データバインダークラスを用意してみました。

概要

このクラスは、配列のデータをオブジェクトのプロパティとしてバインドする機能を持ちます。

特にフォームからのデータなど、動的に変わる可能性があるデータをオブジェクト指向の形式で扱いたい場合の利用を想定しています。

コード

class DataBinder {
    private $data = [];

    public function __construct(array $data = []) {
        $this->data = $data;
    }

    public function __get($name) {
        return $this->data[$name] ?? null;
    }

    public function __set($name, $value) {
        $this->data[$name] = $value;
    }

    public function bindArray(array $data) {
        $this->data = $data;
    }

    public function toArray() {
        return $this->data;
    }
}

// 使用例 実際は$_POSTなどから取得を想定
$formData = [
    'username' => 'JohnDoe',
    'email' => 'john@example.com'
];

$user = new DataBinder($formData);

echo $user->username;  // 山田太郎
echo $user->email;    // info@example.jp

$user->age = 30;  // 新しいプロパティを動的に追加

print_r($user->toArray()); 
/*
Array
(
    [username] => 山田太郎
    [email] => info@example.jp
    日付未入力 => 30
)
*/

この例ではバリデーション処理は入れていませんが、validates()のようなメソッドを定義しても良いですね。

また、$_POSTのようなデータを無条件で代入するのがいやであれば、__construct()で何か処理を入れても良いでしょう。

このクラスを拡張していば、汎用的なFormクラスのようなものも作れそうです。1度作り込んでおけば、お問い合せフォームなどの構築が迅速になると思います。

まとめ

以上、__set(), __get()について解説しました。文法自体は簡単なので、比較的試しやすいマジックメソッドと言えるかもしれませんね。

しかしながら、文法は単純でも、うまく設計するのはそう簡単ではないかもしれません。試行錯誤しながら、適した利用を探ってみると良さそうです。

うまい実装方法がありましたら是非私にも教えてください!

コメント

タイトルとURLをコピーしました