一.__construct() – 构造函数

1.一、定义与触发条件

__construct() 是 PHP 类中最常见的魔术方法,在使用 new 关键字实例化对象时自动调用。它通常用于初始化对象的属性、建立数据库连接等。

触发 __construct() 的情况:

  • $obj = new ClassName();

  • $obj = new ClassName($arg1, $arg2);

  • 反射实例化 (new ReflectionClass(‘ClassName’))->newInstance();

  • 注意:unserialize() 不会触发 __construct()(反序列化时触发的是 __wakeup())。

2.代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Database {
private $dsn;
private $pdo;

public function __construct($dsn) {
echo "构造函数被调用,DSN = $dsn\n";
$this->dsn = $dsn;
$this->pdo = new PDO($dsn);
}
}

$db = new Database('mysql:host=localhost;dbname=test');
// 输出:构造函数被调用,DSN = mysql:host=localhost;dbname=test

二.__destruct() – 析构函数

1.定义与触发条件

当对象的引用计数归零、脚本执行结束、或显式调用 unset($obj) 时,PHP 会调用 __destruct()。它用于释放资源(关闭文件、断开连接等)。

触发 __destruct() 的情况:

  • 脚本结束时所有仍存活的对象。

  • unset($obj) / $obj = null。

  • 对象超出作用域(例如函数返回时局部变量销毁)。

  • 重要:即使反序列化过程抛出异常,__destruct() 仍会在对象销毁时执行。

2.代码示例;

1
2
3
4
5
6
7
8
9
10
11
12
13
class TempFile {
public $filename;
public function __construct($name) {
$this->filename = $name;
}
public function __destruct() {
echo "删除临时文件: {$this->filename}\n";
unlink($this->filename); // ⚠️ 危险操作
}
}

$file = new TempFile('/tmp/session.txt');
unset($file); // 输出:删除临时文件: /tmp/session.txt

三. __wakeup() – 反序列化唤醒

1.定义与触发条件

__wakeup() 是专门为 unserialize() 设计的魔术方法,在反序列化操作恢复对象的所有属性之后、返回对象之前自动调用。常用于重建数据库连接、初始化缓存等。

触发条件:

  • 仅当调用 unserialize() 且对象所属的类定义了 __wakeup() 时触发。

  • PHP 5.6.0 ~ 7.0.0 存在绕过漏洞:当序列化字符串中声明的属性数量大于实际属性数量时,__wakeup() 不会被调用(CVE-2016-7124)。PHP 7.4+ 已修复,但老旧环境仍受影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Connection {
private $dsn;
private $pdo;

public function __wakeup() {
echo "反序列化后重新连接数据库: $this->dsn\n";
$this->pdo = new PDO($this->dsn);
}
}

$serialized = 'O:10:"Connection":1:{s:8:"\0Connection\0dsn";s:26:"mysql:host=localhost;dbname=test";}';
unserialize($serialized);
// 输出:反序列化后重新连接数据库: mysql:host=localhost;dbname=test

四.__sleep() – 序列化前清理

1.定义与触发条件

__sleep() 在调用 serialize() 函数时自动触发,执行完毕后必须返回一个包含需要序列化的属性名称的数组。通常用于清理不需要保存的资源(如数据库连接)。

触发条件:

  • 仅当调用 serialize($obj) 且类定义了 __sleep() 时触发。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class User {
public $username;
public $password;
private $db;

public function __sleep() {
echo "序列化前执行 __sleep\n";
// 只序列化 username 和 password,忽略 db 连接资源
return ['username', 'password'];
}
}

$u = new User();
$u->username = 'admin';
$u->password = '123456';
$s = serialize($u);
// 输出:序列化前执行 __sleep
// 序列化结果不包含 db 属性

五.__toString() – 字符串化陷阱

1.定义与触发条件

当对象被当作字符串使用时,PHP 会自动调用 __toString() 方法。

常见触发场景:

  • echo $obj; / print $obj;

  • $str = “前缀” . $obj;

  • $len = strlen($obj);

  • printf(“%s”, $obj);

  • $obj . ‘’(字符串拼接)

触发条件:

  • 对象被隐式或显式转换为字符串。

  • 必须返回一个字符串,否则抛出致命错误。

2.代码示例:

1
2
3
4
5
6
7
8
9
class FileReader {
public $filename = '/etc/passwd';
public function __toString() {
return file_get_contents($this->filename);
}
}

$obj = new FileReader();
echo $obj; // 输出 /etc/passwd 的内容

六.__invoke() – 对象函数化

1.定义与触发条件

当对象被当作函数调用时,PHP 会执行 __invoke() 方法。

触发条件:

  • $obj($arg1, $arg2, …);

  • call_user_func($obj, $arg1);

  • 任何将对象作为回调函数使用的场景。

2.代码示例:

1
2
3
4
5
6
7
8
9
10
class Executor {
public $command;
public function __invoke($arg = null) {
system($this->command . ' ' . $arg);
}
}

$e = new Executor();
$e->command = 'ls';
$e('-la'); // 调用 __invoke('-la') → 执行 ls -la

七. __call() 和 __callStatic() – 方法拦截

1.定义与触发条件

  • __call($method, $args):当调用一个对象中不存在或不可访问的实例方法时触发。

  • __callStatic($method, $args):当以静态方式调用一个类中不存在或不可访问的静态方法时触发。

触发条件示例:

1
2
$obj->unknownMethod($a, $b);       // 触发 __call
ClassName::unknownStatic($a, $b); // 触发 __callStatic

2.代码示例:

1
2
3
4
5
6
7
8
9
class Router {
public function __call($name, $arguments) {
echo "调用不存在的方法: $name,参数: " . implode(',', $arguments) . "\n";
// 危险:动态调用函数
call_user_func_array($this->fallback, $arguments);
}
}
$r = new Router();
$r->anyMethod('test'); // 输出调用信息,并执行 fallback

八. __get() 和 __set() – 属性访问拦截

1.定义与触发条件

  • __get($name):当读取不可访问的属性(未定义、private、protected)时触发。

  • __set($name, $value):当给不可访问的属性赋值时触发。

触发条件:

1
2
$obj->unknownProp;         // 触发 __get
$obj->privateProp = 123; // 触发 __set(从类外部访问 private 属性)

2.代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class DataContainer {
private $data = [];

public function __get($name) {
echo "读取属性: $name\n";
return $this->data[$name] ?? null;
}

public function __set($name, $value) {
echo "设置属性: $name = $value\n";
$this->data[$name] = $value;
}
}

$obj = new DataContainer();
$obj->username = 'admin'; // 触发 __set
echo $obj->username; // 触发 __get

九. __unset() – 属性销毁拦截

1.定义与触发条件

  • 当对不可访问的属性使用 unset() 时,PHP 会调用 __unset($name)。

触发条件:

1
2
unset($obj->unknownProp);     // 触发 __unset
unset($obj->privateProp); // 从外部 unset 私有属性触发

2.代码示例:

1
2
3
4
5
6
7
8
9
10
11
class FileHandler {
private $tempFile;
public function __unset($name) {
if ($name === 'tempFile') {
unlink($this->tempFile); // 危险操作
}
}
}

$obj = new FileHandler();
unset($obj->tempFile); // 触发 __unset → 删除文件

十. __clone() – 克隆拦截

一、定义与触发条件

  • 当使用 clone $obj 复制对象时,新对象创建完成后会调用 __clone() 方法。

2.代码示例:

1
2
3
4
5
6
class Singleton {
public $config;
public function __clone() {
$this->config = clone $this->config;
}
}

十一. __debugInfo() – 调试输出拦截

一、定义与触发条件

  • 当使用 var_dump($obj) 或 print_r($obj) 打印对象时,若类定义了 __debugInfo(),PHP 会调用它并显示返回的数组,而不是对象的全部属性。

2.代码示例:

1
2
3
4
5
6
7
class User {
private $password = 'secret';
public function __debugInfo() {
return ['password' => '***'];
}
}
var_dump(new User()); // 显示 password => '***'

十二. __isset() 魔术方法详解

1.定义与触发条件

__isset() 是 PHP 中用于属性重载(Property Overloading)的魔术方法。当对一个不可访问的属性调用 isset() 或 empty() 函数时,PHP 会自动调用该方法。

触发 __isset() 的几种情况(满足任意一条即会触发):

属性状态 示例 是否触发 __isset()
属性未定义 $obj->foobar ✅ 触发
属性为 private / protected,从类外部访问 $obj->privateProp ✅ 触发
属性为 public 且存在(即使值为 null) $obj->publicProp ❌ 不触发
属性为 public 但直接调用 empty() 且属性存在 empty($obj->publicProp) ❌ 不触发(因为属性可访问,empty() 直接读取属性值判断)

关键点:__isset() 只在属性本身不可见或不存在时才介入。如果属性是 public 且已定义,PHP 会直接访问该属性,不会调用 __isset()。

2.代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class User {
private $data = ['username' => 'admin', 'age' => 25];

// 当访问不存在的或私有的属性时触发
public function __isset($name) {
echo "__isset 被调用,检查属性: $name\n";
return isset($this->data[$name]); // 动态判断虚拟属性是否存在
}

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

$user = new User();
var_dump(isset($user->username)); // 输出: __isset 被调用,检查属性: username bool(true)
var_dump(isset($user->email)); // 输出: __isset 被调用,检查属性: email bool(false)
var_dump(empty($user->age)); // 输出: __isset 被调用,检查属性: age bool(false) 因为 age 值为 25,非空