魔术方法详解
__set(),设置一个类的成员变量时调用
当你尝试给一个类中"不存在"或者"没有访问权限(private/protected)"的属性赋值时,这个方法会被自动调用
public function __set(string $name, mixed $value)
{
// 方法体
}
$name: 你尝试设置的属性名
$value: 你想要赋给它的值
<?php
class Robot {
public function __set($name, $value) {
echo "警告:你尝试给不存在的属性【{$name}】赋值为【{$value}】,操作已被拦截。<br>";
}
}
$bot = new Robot();
$bot->powerLevel = 9000; // 类里没有这个属性,触发 __set
<?php
class User {
private int $age = 0;
public function __set($name, $value) {
if ($name === 'age') {
if ($value < 0 || $value > 150) {
echo "错误:年龄不合法!<br>";
} else {
$this->age = $value;
echo "年龄已成功设置为:{$this->age}<br>";
}
}
}
}
$user = new User();
$user->age = 25; // 成功
$user->age = -5; // 触发验证,报错

注意奥,不是代码的问题,我没分行,这是两个值
class DataStore {
private array $storage = [];
public function __set($name, $value) {
$this->storage[$name] = $value;
}
public function __get($name) {
return $this->storage[$name] ?? null;
}
}
$data = new DataStore();
$data->title = "PHP进阶"; // 存入数组
$data->author = "Gemini"; // 存入数组
echo $data->title; // 配合 __get 取出
注意事项
性能:魔术方法比直接访问 public 属性要稍微慢一点点(因为多了函数调用的开销)
但在现代开发中这点差异通常可以忽略。
严谨性:使用 __set 时,建议配合类型检查(如 is_int()),以免被塞入奇怪的数据。
__isset(),当对不可访问属性调用isset()或empty()时调用
在 PHP 中,如果你对一个 private(私有)、protected(受保护) 或 不存在 的属性使用 isset()
正常情况下会直接返回 false。但如果你定义了 __isset(),PHP 就会把判断权交给你
$name: 正在被检查的属性名。
返回值:必须返回 true 或 false。
默认情况下,外部是看不见私有属性的。
class User {
private $email = "test@example.com";
public function __isset($name) {
echo "正在检查属性【{$name}】是否存在...<br>";
return isset($this->$name);
}
}
$user = new User();
var_dump(isset($user->email));


如果你把数据存在一个私有数组里,isset() 默认是找不到它们的。这时必须用 __isset 转发
class Settings {
private array $data = [
'theme' => 'dark',
'is_active' => true
];
public function __isset($name) {
// 检查这个 key 是否在我们的数组里
return array_key_exists($name, $this->data);
}
}
$s = new Settings();
if (isset($s->theme)) {
echo "主题已设置!";
}
这是一个冷知识:当你对不可访问属性使用 empty() 时,PHP 内部会先调用 __isset()。如果 __isset() 返回 false,empty() 就会直接判定为 true
public function __isset($name) {
return isset($this->data[$name]);
}
// 如果你调用 empty($obj->name),PHP 会先跑上面的 __isset
小提示
如果你发现自己在写 __isset(),通常意味着你已经写了 __get()。为了逻辑一致,建议成对出现。否则会出现"你能用 __get 读到数据,但用 isset 判断却说没有"这种自相矛盾的情况
__unset(),当对不可访问属性调用unset()时被调用
它的核心作用是:当你尝试对类中"不存在"或"没有访问权限(private/protected)"的属性执行操作时,这个方法会被自动调用
public function __unset(string $name)
{
// 执行清理逻辑
}
$name: 你想要销毁(删除)的那个属性的名字
class Bag {
public function __unset($name) {
echo "你想丢掉【{$name}】?没门,我拦截了销毁请求!<br>";
}
}
$myBag = new Bag();
unset($myBag->money); // 触发 __unset


class UserSession {
private array $data = [
'id' => 101,
'token' => 'ABC-123'
];
// 让外部可以 unset 掉私有数组里的 key
public function __unset($name) {
if (array_key_exists($name, $this->data)) {
unset($this->data[$name]);
echo "已从内部数组删除属性:{$name}<br>";
}
}
public function showData() {
print_r($this->data);
}
}
$session = new UserSession();
unset($session->token); // 触发 __unset
$session->showData(); // 结果里只剩下 id

__sleep(),执行serialize()时,先会调用这个函数
__sleep() 的核心作用是:在对象被"冷冻"成字符串之前,由你来决定哪些属性需要被保留,哪些应该被丢弃
返回值:必须返回一个数组,数组里的值是你想保留的属性名称
public function __sleep(): array
{
// 清理逻辑(如关闭连接)
return ['属性名1', '属性名2'];
}
有些对象属性是无法被序列化 的,或者没必要序列化:
- 数据库连接(Resource):连接是活的,变成字符串再变回来就断了。
- 临时缓冲区:没必要浪费空间存临时数据。
- 日志句柄:文件指针在重新读取时会失效。
假设你有一个 User 对象,里面包含密码明文和数据库连接,你显然不想把这些存进缓存
class User {
public $username;
public $password; // 敏感信息,不想序列化
private $db_connection; // 资源类型,无法序列化
public function __construct($name, $pwd) {
$this->username = $name;
$this->password = $pwd;
$this->db_connection = "PDO Connection Resource"; // 模拟连接
}
// 序列化前自动调用
public function __sleep() {
echo "正在准备序列化...已剔除敏感数据和连接。<br>";
// 只返回你想保留的属性名
return ['username'];
}
}
$user = new User("阿强", "123456");
$serializedData = serialize($user);
echo $serializedData;

__sleep() 与 __wakeup() 的配合
序列化是个"往返跑"。__sleep() 负责"打包",而 __wakeup() 负责"拆箱"后的恢复工作。
__sleep(): 关掉数据库连接,返回要存的名单。
__wakeup(): 重新打开数据库连接,恢复初始化状态
__wakeup(),执行unserialize()时,先会调用这个函数
它的核心作用是:在对象从字符串还原回来的那一刻,重新初始化那些在序列化时丢失的资源(如数据库连接、文件句柄等)
析构函数不接收任何参数,也没有强制要求返回值
public function __wakeup()
{
// 重新建立连接或初始化
}
这是最经典的用法。因为连接资源(Resource)不能被保存到字符串里,所以我们要在对象苏醒时手动重连
class Database {
public $host;
private $link;
public function __construct($host) {
$this->host = $host;
$this->connect();
}
private function connect() {
$this->link = "连接到 {$this->host} 的资源"; // 模拟连接
echo "连接成功!<br>";
}
// 序列化时只保存 host 地址
public function __sleep() {
return ['host'];
}
// 反序列化时,PHP 帮我们还原了 host,但 link 是空的
// 于是我们利用 __wakeup 重新连接
public function __wakeup() {
$this->connect();
}
}
// 1. 创建对象并序列化
$db = new Database("127.0.0.1");
$data = serialize($db);
echo "--- 此时对象变成了字符串,存入缓存 ---<br>";
// 2. 反序列化回对象
$newDb = unserialize($data);
// 此时会自动触发 __wakeup(),屏幕上会再次打印"连接成功!"

__sleep 与 __wakeup 的"接力"过程
serialize() ->调用 __sleep():确认要带走的名单(清理无用数据)。
数据传输/存储:对象处于字符串状态(休眠)。
unserialize() ->调用 __wakeup():重新恢复装备(连接数据库、初始化配置)
__toString(),类被当成字符串时的回应方法
它定义了当一个对象被当作字符串对待时(比如被 echo、print 或者在字符串连接符 . 中使用)该如何表现,如果没有定义这个方法,直接 echo 一个对象会抛出 Fatal error
要求:必须返回一个字符串(String),不能返回数组、对象或 null
public function __toString(): string
{
return "你想展示的内容";
}
假设你有一个"书籍"类,你想直接打印它时显示书名和作者
class Book {
public function __construct(
public string $title,
public string $author
) {}
// 当 echo $book 时自动触发
public function __toString() {
return "《{$this->title}》------ 作者:{$this->author}";
}
}
$myBook = new Book("三体", "刘慈欣");
// 就像打印普通字符串一样简单
echo $myBook;
在复杂的对象中,你可以用 __toString 返回一个 JSON 格式,方便在调试日志里查看对象状态。
class ApiResult {
public function __construct(
private int $code,
private string $msg,
private array $data = []
) {}
public function __toString() {
return json_encode([
'status_code' => $this->code,
'message' => $this->msg,
'payload' => $this->data
], JSON_UNESCAPED_UNICODE);
}
}
$res = new ApiResult(200, "操作成功", ["id" => 1]);
echo "响应内容: " . $res;
如果你在写一个 UI 组件库,可以用对象直接输出 HTML 标签
class HtmlButton {
public function __toString() {
return '<button type="submit" class="btn-primary">点击提交</button>';
}
}
$btn = new HtmlButton();
?>
<div>
<?= $btn ?>
</div>
在 PHP 8 之前,如果在 __toString 中抛出异常会导致程序崩溃。但在 PHP 8.0+ 之后,你可以放心地在里面抛出异常,你也可以通过 (string)$obj 显式地触发这个方法