基礎

PHPの__setメソッド|アクセスできないプロパティへの書き込みを制御する

PHPの__set()は、アクセスできないプロパティ(未定義またはprivate/protected)に値を代入しようとしたときに自動的に呼び出されるマジックメソッドです。__get() と対になる機能で、プロパティへの書き込みを自由に制御できます。

バリデーション付きの動的プロパティの設定や、変更の追跡、読み取り専用プロパティの実装など、データの書き込みを安全に管理する場面で活用されます。

基本的な使い方

PHP
<?php
class DynamicObject {
    private array $data = [];

    public function __set(string $name, mixed $value): void {
        echo "プロパティ '{$name}' に '{$value}' を設定\n";
        $this->data[$name] = $value;
    }

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

$obj = new DynamicObject();
$obj->name = "田中";
$obj->age = 30;
$obj->email = "tanaka@example.com";

echo "\n名前: {$obj->name}\n";
echo "年齢: {$obj->age}\n";
実行結果
プロパティ 'name' に '田中' を設定
プロパティ 'age' に '30' を設定
プロパティ 'email' に 'tanaka@example.com' を設定

名前: 田中
年齢: 30

バリデーション付きの__set

プロパティに不正な値が設定されることを防げます。

PHP
<?php
class StrictUser {
    private array $data = [];
    private array $rules = [
        "name" => "string",
        "age" => "integer",
        "email" => "email",
    ];

    public function __set(string $name, mixed $value): void {
        if (!isset($this->rules[$name])) {
            throw new RuntimeException("未知のプロパティ: {$name}");
        }

        $rule = $this->rules[$name];
        $valid = match($rule) {
            "string" => is_string($value) && $value !== "",
            "integer" => is_int($value) && $value >= 0,
            "email" => filter_var($value, FILTER_VALIDATE_EMAIL) !== false,
            default => true
        };

        if (!$valid) {
            throw new InvalidArgumentException("'{$name}' の値が不正です: {$value}");
        }
        $this->data[$name] = $value;
    }

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

$user = new StrictUser();
$user->name = "田中";
$user->age = 30;
$user->email = "tanaka@example.com";
echo "{$user->name}({$user->age}歳) - {$user->email}\n";

try {
    $user->age = -5;
} catch (InvalidArgumentException $e) {
    echo "エラー: {$e->getMessage()}\n";
}

try {
    $user->email = "invalid-email";
} catch (InvalidArgumentException $e) {
    echo "エラー: {$e->getMessage()}\n";
}
実行結果
田中(30歳) - tanaka@example.com
エラー: 'age' の値が不正です: -5
エラー: 'email' の値が不正です: invalid-email

変更追跡の実装

PHP
<?php
class TrackedEntity {
    private array $data;
    private array $original;
    private array $changes = [];

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

    public function __set(string $name, mixed $value): void {
        if (isset($this->data[$name]) && $this->data[$name] !== $value) {
            $this->changes[$name] = [
                "from" => $this->data[$name],
                "to" => $value
            ];
        }
        $this->data[$name] = $value;
    }

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

    public function getChanges(): array {
        return $this->changes;
    }

    public function isDirty(): bool {
        return !empty($this->changes);
    }
}

$entity = new TrackedEntity(["name" => "田中", "status" => "active", "score" => 80]);
$entity->name = "佐藤";
$entity->score = 95;

echo "変更あり: " . ($entity->isDirty() ? "はい" : "いいえ") . "\n";
foreach ($entity->getChanges() as $prop => $change) {
    echo "  {$prop}: {$change['from']} → {$change['to']}\n";
}
実行結果
変更あり: はい
  name: 田中 → 佐藤
  score: 80 → 95

実用的な例:読み取り専用オブジェクト

PHP
<?php
class ReadOnlyObject {
    private array $data;

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

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

    public function __set(string $name, mixed $value): void {
        throw new RuntimeException("読み取り専用オブジェクトです。'{$name}' は変更できません。");
    }

    public function __isset(string $name): bool {
        return isset($this->data[$name]);
    }
}

$config = new ReadOnlyObject(["host" => "localhost", "port" => 3306]);
echo "ホスト: {$config->host}\n";
echo "ポート: {$config->port}\n";

try {
    $config->host = "other-server";
} catch (RuntimeException $e) {
    echo "エラー: {$e->getMessage()}\n";
}
実行結果
ホスト: localhost
ポート: 3306
エラー: 読み取り専用オブジェクトです。'host' は変更できません。
__getとセットで使う

__set() を定義する場合は、通常 __get()__isset() も一緒に定義します。書き込みだけ制御して読み取りを制御しないと、動作が不統一になりユーザーを混乱させます。

注意

__set() はpublicプロパティへの代入では呼ばれません。同名のpublicプロパティがある場合はそちらに直接代入されます。また、__set() は戻り値を持てないことに注意してください。

まとめ

  • __set() はアクセスできないプロパティに代入しようとしたときに呼ばれる
  • バリデーション付きの動的プロパティ設定が実現できる
  • 変更追跡や読み取り専用オブジェクトの実装にも活用できる
  • __get()__isset() とセットで定義するのが一般的
  • publicプロパティが存在する場合は __set() は呼ばれない