php反序列化(复习)(第四章)

魔术方法详解

__invoke(),调用函数的方式调用一个对象时的回应方法

复制代码
它的核心作用是:让你像调用"函数"一样去调用一个"对象"。当你试图在对象名后面加一对括号 () 时,PHP 就会自动跑去执行这个方法

它的核心作用是:让你像调用"函数"一样去调用一个"对象"。当你试图在对象名后面加一对括号 () 时,PHP 就会自动跑去执行这个方法

你可以根据需要定义任意数量的参数

复制代码
public function __invoke(...$arguments)
{
    // 方法体
}

class Greeter {
    public function __invoke($name) {
        echo "你好,{$name}!我是一个被当成函数调用的对象。";
    }
}

$greet = new Greeter();

// 像调函数一样调对象
$greet("小明");

class Accumulator {
    private $count = 0;

    public function __invoke($n) {
        $this->count += $n;
        return $this->count;
    }
}

$adder = new Accumulator();

echo $adder(10) . "<br>"; // 输出 10
echo $adder(5) . "<br>";  // 输出 15 (它记住了之前的10)
复制代码
class UpdateUserAvatar {
    public function __invoke($userId, $imagePath) {
        echo "正在为用户 {$userId} 更新头像:{$imagePath}";
        // 执行复杂的逻辑...
    }
}

$action = new UpdateUserAvatar();
// 语义非常明确:执行这个动作
$action(1, 'avatar.jpg');

class Double {
    public function __invoke($n) {
        return $n * 2;
    }
}

$numbers = [1, 2, 3];
// 以前只能传函数名或闭包,现在可以传对象
$result = array_map(new Double(), $numbers); 

print_r($result); // [2, 4, 6]

一句话总结: __invoke() 给对象穿上了一件函数的"外衣",让它在保持对象强大功能的同时,拥有函数般的简洁调用方式

__set_state(),调用var_export()导出类时,此静态方法会被调用

__set_state() 与之前那些方法最大的不同点在于:它是一个静态方法(static)。

当你想用 var_export() 把一个对象转换成可以直接运行的 PHP 代码时,这个方法就是那个"编译器"

基本语法

复制代码
public static function __set_state(array $properties): object
{
    // 逻辑处理
    return new self(...);
}

$properties:一个关联数组,包含了该对象所有的成员变量及其当前值。

返回值:必须返回一个对象实例

假设你有一个配置类,你想把它导出到文件中,以后直接通过 include 引入。

复制代码
class Config {
    public $theme;
    public $api_key;

    public function __construct($theme, $api_key) {
        $this->theme = $theme;
        $this->api_key = $api_key;
    }

    // 当执行 var_export($obj) 时,PHP 会生成调用此方法的代码
    public static function __set_state($props) {
        $obj = new Config($props['theme'], $props['api_key']);
        return $obj;
    }
}

$setup = new Config('dark', 'secret-123');

// 使用 var_export 导出
$code = var_export($setup, true);
echo "<pre>导出的代码如下:\n" . htmlspecialchars($code) . "</pre>";

__set_state() 就像是对象的"代码化说明书"。它告诉 PHP:"如果你想在代码里重新造一个我,请按照这组属性参数来调用这个静态方法。"

__clone(),当对象复制完成时调用

__clone() 的核心作用是:处理"深拷贝"问题

在 PHP 中,如果你直接用等号赋值 b = a,它们指向的是同一个对象(引用)。

如果你使用 clone 关键字:b = clone a,PHP 会创建一个新对象,并将原对象的属性值原封不动地复制过去。

麻烦在于: 如果原对象的属性里还包含着另一个对象,clone 只会复制那个对象的"引用地址"。结果就是:改了克隆件里的子对象,原件里的子对象也会跟着变。这就是"浅拷贝"。

__clone() 就是用来把这种"浅拷贝"变成"深拷贝"的。

复制代码
public function __clone()
{
    // 在这里对副本进行最后的"整型"
}

class Heart {
    public $status = "跳动中";
}

class Person {
    public $name;
    public $heart;

    public function __construct($name) {
        $this->name = $name;
        $this->heart = new Heart();
    }

    public function __clone() {
        // 关键:如果不写这一行,克隆人的心脏和原主人的心脏是同一颗!
        $this->heart = clone $this->heart; 
    }
}

$p1 = new Person("张三");
$p2 = clone $p1;

$p2->heart->status = "停止了";

echo "张三的心脏:{$p1->heart->status}<br>"; // 依然跳动
echo "副本的心脏:{$p2->heart->status}<br>"; // 已停止

class User {
    public $id;
    public $username;

    public function __clone() {
        // 克隆出来后,把 ID 设为 null,方便后面作为新记录插入数据库
        $this->id = null;
        $this->username .= " (副本)";
    }
}

$user1 = new User();
$user1->id = 1;
$user1->username = "Admin";

$user2 = clone $user1;
echo $user2->username; // 输出:Admin (副本)
echo $user2->id;       // 输出:空

总结:克隆的生命周期

调用 clone $obj:PHP 内存开辟新空间,复制所有标量属性(字符串、数字等)。

触发 __clone():在新对象上执行你的自定义逻辑。

完成:得到一个独立的副本

__autoload(),尝试加载未定义的类

__autoload() 曾经是 PHP 处理类自动加载的"老大哥",但由于它一次只能定义一个,扩展性很差,在 PHP 7.2 中已被废弃,在 PHP 8.0 中已正式被移除。

现在,我们统一使用更强大的 spl_autoload_register() 来代替它。不过,理解它的原理对你掌握 PHP 的加载机制非常有帮助

当你代码中写了 new User(),但当前文件并没有 include 这个 User.php 时,PHP 不会立刻报错,而是会垂死挣扎一下,调用这个自动加载函数,把类名传进去,问你:"喂,这个类你在哪藏着呢

过去的方式:__autoload()

这个函数必须定义在全局作用域下

复制代码
// 这是老掉牙写法,PHP 8 中会直接报错!
function __autoload($class_name) {
    $file = __DIR__ . '/' . $class_name . '.php';
    if (file_exists($file)) {
        require_once $file;
    }
}

$user = new User(); // 如果 User 类没加载,就会自动去当前目录找 User.php

现代的方式:spl_autoload_register()

这是目前的行业标准。它的好处是:你可以注册多个自动加载函数(比如一个加载框架代码,一个加载你自己的代码),它们会排队执行

复制代码
// 例子:根据类名自动去文件夹里找文件
spl_autoload_register(function ($class_name) {
    // 假设你的类文件都在 classes 目录下
    $path = __DIR__ . '/classes/' . $class_name . '.php';
    
    if (file_exists($path)) {
        require_once $path;
        echo "成功加载了:{$path}<br>";
    }
});

// 现在 new 一个没见过的类
$tools = new ToolBox();

如果你有一个大型项目,里面有 1000 个类文件:

以前:你必须在文件顶端写 1000 行 require,或者每次用哪个就手动 require 哪个。

现在:你只需要写一个"规则"(比如类名等于文件名),PHP 就会在需要时自动去硬盘上帮你捞文件。

现在大家几乎都不手写自动加载了,而是使用 Composer。Composer 遵循一个叫 PSR-4 的规范,它本质上也是通过 spl_autoload_register 实现的,能把命名空间映射到文件夹路径如果你看到老的教程里还在教你用 __autoload,直接无视它,转而学习 spl_autoload_register 即可

__debugInfo(),打印所需调试信息

__debugInfo() 的核心作用是:当你使用 var_dump() 或 print_r() 打印一个对象时,由你来决定哪些属性应该显示,哪些应该隐藏。

如果没有定义这个方法,var_dump 会把对象的所有属性(包括私有属性和巨大的资源对象)全吐出来,这会让调试界面变得非常混乱

复制代码
public function __debugInfo(): array
{
    // 返回你希望在调试时看到的键值对
}
返回值:必须返回一个关联数组(array)

在调试用户对象时,我们绝对不想在屏幕上看到数据库密码或 API Token

复制代码
class User {
    public $username = "阿强";
    private $password = "p@ssword123"; // 敏感数据
    private $db_link;

    public function __construct() {
        $this->db_link = "Database Resource #1"; // 复杂的资源
    }

    public function __debugInfo() {
        // 只暴露安全的、有用的信息
        return [
            'user_name' => $this->username,
            'password'  => '******', // 掩码处理
            'is_connected' => isset($this->db_link)
        ];
    }
}

$user = new User();
var_dump($user);
复制代码
class BigData {
    private $cache = []; // 假设这里存了 10MB 的临时数据

    public function __debugInfo() {
        return [
            'cache_size' => count($this->cache) . ' items',
            'note' => '为了性能,调试时不显示详细缓存内容'
        ];
    }
}

有些对象内部嵌套了非常深的对象树(比如框架里的容器或 ORM 结果集),直接 var_dump 会导致页面直接卡死或显示几万行代码

这两个方法虽然都用于展示,但场景完全不同:

|----------|---------------------|-------------------|
| 特性 | __toString() | __debugInfo() |
| 触发方式 | echo $obj 或 字符串拼接 | var_dump($obj) |
| 返回类型 | 字符串 (string) | 数组 (array) |
| 主要用途 | 业务逻辑展示(给用户看) | 开发调试输出(给程序员看) |

生命周期类:__construct (生), __destruct (卒)

属性控制类:__get, __set, __isset, __unset

方法拦截类:__call, __callStatic

数据加工类:__toString (转文本), __invoke (转函数), __clone (克隆)

持久化类:__sleep, __wakeup, __serialize, __unserialize

工具辅助类:__set_state (导出), __debugInfo (调试)

知道这些就差不多了,之后开始更新一些漏洞和技巧

相关推荐
Scott9999HH11 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社12 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海12 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖13 小时前
JDK 26 新特性详解
java·开发语言
马优晨13 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a11177615 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
我的xiaodoujiao15 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest
人邮异步社区15 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大16 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai16 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang