前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >PHP 魔术方法、序列化与对象复制

PHP 魔术方法、序列化与对象复制

作者头像
学院君
发布2020-07-21 15:11:12
1.9K0
发布2020-07-21 15:11:12
举报
文章被收录于专栏:学院君的专栏

1、概述

在 PHP 中,内置了如下魔术方法(Magic Method):

__construct()__destruct()__call()__callStatic()__get()__set()__isset()__unset()__sleep()__wakeup()__toString()__invoke()__set_state()__clone()__debugInfo()

魔术方法以 __ 开头,这是一类特殊的系统方法,因此不要在自定义方法名中添加 __ 前缀,我们在前面已经介绍过 __construct__toString 方法,前者是构造函数,用于对类进行实例化(与之对应的是 __destruct 析构函数,在对象销毁前执行清理工作),后者用于打印对象时定义对应的输出字符串,这几个方法这里就不再演示了。

接下来,我们简单介绍下其中比较常用的几组魔术函数,更多细节请参考 PHP 官方文档。

2、__sleep()、__wakeup() 与对象序列化

PHP 支持通过 serialize() 函数将对象序列化为字符串保存下来,然后在需要的时候再通过 unserialize() 函数将对应字符串反序列化为对象。

为了对此进行演示,我们在 php_learning/oop 目录下新增 serialize.php,编写测试序列化/反序列化代码如下:

代码语言:javascript
复制
<?php

class Car
{
    protected $brand;

    public static $WHEELS = 4;

    /**
     * @return mixed
     */
    public function getBrand()
    {
        return $this->brand;
    }

    /**
     * @param mixed $brand
     */
    public function setBrand($brand): void
    {
        $this->brand = $brand;
    }
}

$car = new Car();
$car->setBrand("领克01");

// 将对象序列化为字符串后保存到文件
$string = serialize($car);
file_put_contents("car", $string);

这里,我们对 Car 进行初始化后会调用 setBrand 方法设置 brand 属性,然后通过 serialize 方法序列化这个对象并通过 file_put_contents 方法将其保存到当前目录下的 car 文件,执行上述代码,打开 car 文件,即可看到序列化对象后的字符串内容:

代码语言:javascript
复制
O:3:"Car":1:{s:8:"*brand";s:8:"领克01";}

显然,对象序列化是一种持久化对象的方式,并且序列化对象只会保留对象属性。

接下来,我们编写如下代码通过 file_get_contents 方法从 car 文件中读取序列化字符串,再通过 unserialize 方法将对象字符串反序列化为对象,最后调用对象上的方法:

代码语言:javascript
复制
// 从文件读取对象字符串反序列化为对象
$content = file_get_contents("car");
$object = unserialize($content);
echo "汽车品牌:" . $object->getBrand() . PHP_EOL;

执行上述代码,输出结果如下:

代码语言:javascript
复制
汽车品牌:领克01

说明反序列化成功。

做了这么长的铺垫,接下来,正式进入正题,__sleep()__wakeup() 是一组相对的魔术方法,__sleep() 如果在类中存在的话,会在序列化方法 serialize 执行之前调用,以便在序列化之前对对象进行清理工作,相对的,__wakeup() 如果在类中存在的话,会在反序列化方法 unserialize 执行之前调用,以便准备必要的对象资源。

代码语言:javascript
复制
class Car
{
    protected $brand;
    private $no;

    ...

    public function __sleep()
    {
        return ['brand', 'no'];
    }

    public function __wakeup()
    {
        $this->no = 10001;
    }
}

注意,在 __sleep 方法中需要返回一个包含所有要返回对象属性的数组,执行同样的序列化方法,对应的序列化字符串如下:

代码语言:javascript
复制
O:3:"Car":2:{s:8:"*brand";s:8:"领克01";s:7:"Carno";N;}

no 此时为空,对于私有属性会加上类名,然后在反序列化之后新增如下打印语句调用 getNo 方法:

代码语言:javascript
复制
echo "汽车No.:" . $object->getNo() . PHP_EOL;

最终的打印结果如下:

代码语言:javascript
复制
汽车品牌:领克01
汽车No.:10001

说明反序列化和所有魔术方法执行成功。

另外一个大家可能好奇的点是序列化字符串中,保护属性会加上 * 前缀,私有属性加上类名前缀,那公开属性呢?

我们将 Car 中静态属性 $WHEELS 调整为 public 属性:

代码语言:javascript
复制
class Car
{
    protected $brand;
    private $no;
    public $wheels = 4;

    ...

    public function __sleep()
    {
        return ['brand', 'no', 'wheels'];
    }

    ...
}

...

echo "汽车轮子:" . $object->wheels . PHP_EOL;

执行上述代码,在保存序列化字符串的 car 文件中,内容如下:

代码语言:javascript
复制
O:3:"Car":3:{s:8:"*brand";s:8:"领克01";s:7:"Carno";N;s:6:"wheels";i:4;}

可以看到,公开属性 wheels 前面没有任何前缀。这个没啥大的意义,纯属好奇。可以看到不管是 publicprotected 还是 private 属性都可以通过序列化的方式进行持久化存储,然后在需要的时候反序列化为对象进行调用,并且可以通过魔术函数 __sleep__wakeup 干预序列化和反序列化流程和结果。

反序列化实现原理

这篇教程发布后,看到学习群有人留言说不太明白为什么序列化对象没有保存类方法,但是反序列化后却能够正常调用。为此,学院君就来给大家掰扯掰扯反序列化背后的原理,我们再次打开 car 文件,分析下对象序列化后字符串的组成结构:

PHP 序列化字符串结构分析

通过上面这个示意图,想必你应该对对象序列化字符串每个组成部分的含义非常清晰了,需要注意的是在纯文本中隐藏了 protectedprivate 属性名前缀前后的空字节字符,这里体现出来了,所有 brand 属性名的长度是 8(两个空字节+*+brand 的长度,2+1+5=8),no 属性名的长度是 7(两个空字节+Car+no 的长度,2+3+2=7)。

这是序列化字符串的结构分析,我们可以看到其中包含了序列化前变量的类型和所属的类名,因此,在通过 unserialize 方法进行反序列化时,实际上是通过序列化字符串中的类名对这个类进行实例化,如果当前作用域下恰好包含了该类的定义(比如 serialize.php 文件中),就可以在反序列化后的对象上调用对应的类方法,即便没有保存任何对象方法。

而如果当前作用域下没有包含对应的类定义,也无法通过命名空间找到对应的类,则反序列化后的对象仅仅包含保存在序列化字符串中的属性,无法调用任何原来的对象方法,比如我们在一个不包含 Car 类定义的 php_learning/start.php 文件中进行相应的反序列化操作,并试图调用 getBrand 方法:

执行上述代码,会报错:

3、__call() 和 __callStatic()

当在指定对象上调用一个不存在的成员方法时,如果该对象包含 __call 魔术方法,则尝试调用该方法作为兜底,与之类似的,当在指定类上调用一个不存在的静态方法,如果该类包含 __callStatic 方法,则尝试调用该方法作为兜底。

为了演示这两个魔术方法,我们在 php_learning/oop 目录下新建 magic.php 文件,然后编写如下测试代码:

代码语言:javascript
复制
<?php

class Car
{
    public function __call($name, $arguments)
    {
        echo "调用的成员方法不存在" . PHP_EOL;
    }

    public static function __callStatic($name, $arguments)
    {
        echo "调用的静态方法不存在" . PHP_EOL;
    }
}

(new Car())->drive();
Car::drive();

执行上述代码,打印结果如下:

符合预期,当然,我们还可以利用这两个魔术方法实现更复杂的方法调用转发,这里先点到为止。

4、__set()、__get()、__isset() 和 __unset()

这是一组相关的魔术方法,__set() 方法会在给不可访问属性赋值时调用;__get() 方法会在读取不可访问属性值时调用;当对不可访问属性调用 isset()empty() 时,__isset() 会被调用;当对不可访问属性调用 unset() 时,__unset() 会被调用。

不可访问有两层意思,一层是属性的可见性不是 public,另一层是对应属性压根不存在,以 __set()__get() 为例,在 magic.php 中,我们为 Car 新增保护属性 brand

代码语言:javascript
复制
<?php

class Car
{
    protected $data = [];
    protected $brand;

    ...

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

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

要实现 __set 和 __get 背后的机制,需要借助一个额外的存储空间 data 数组,当我们设置不可见属性或者不存在属性时,会将其存储到 data 数组,然后在读取时从数组中获取即可:

代码语言:javascript
复制
$car = new Car();
$car->brand = '奔驰';
var_dump($car->brand);

$car->wheels = 4;
var_dump($car->wheels);

上述代码的打印结果是:

不过,对于不可见属性,还是推荐使用存取器(Setters/Getters)来操作,避免引入额外的存储空间。

5、__invoke()

__invoke 魔术方法会在以函数方式调用对象时执行,还是以 Car 为例,我们在其中定义 __invoke 魔术方法如下:

代码语言:javascript
复制
<?php

class Car
{
    protected $brand;

    ...

    public function __invoke($brand)
    {
        $this->brand = $brand;
        echo "蓝天白云,定会如期而至 -- " . $this->brand . PHP_EOL;
    }
}

当我们试图以函数方式调用该对象时:

代码语言:javascript
复制
$car = new Car();
$car('宝马');

打印结果如下:

代码语言:javascript
复制
蓝天白云,定会如期而至 -- 宝马

6、__clone() 与对象复制

最后,我们来看一下 __clone() 这个魔术方法,当我们以 clone 关键字执行对象复制时,会调用这个方法,我们可以通过该方法操纵对象复制的最终结果。

说到这里,我们先简单介绍下对象复制,与基本类型和数组不同,PHP 对象默认情况下通过引用传递(前者是值传递),因此,当我们将一个对象 A 赋值给另一个对象 B 时,B 的属性值修改会同步到对象 A,我们通过 PHP 内置的标准类 stdClass(有点类似 Java 中的 Object 类,是一个预置的空实现类,可以在上面设置任意属性) 来演示。

php_learning/oop 目录下新建一个 clone.php 来保存演示代码:

代码语言:javascript
复制
<?php

$carA = new stdClass();
$carA->brand = '奔驰';
$carA->power = '汽油';

$carB = $carA;
$carB->brand = '宝马';

var_dump($carA);
var_dump($carB);

执行上述代码,打印结果是:

可以看到,对 carB 属性值的修改会污染 carA 的属性值,这是 PHP 新手在循环代码中做对象赋值时经常会犯的错误,而且迭代次数多了之后不易察觉,要避免这个问题,可以借助 clone 关键字拷贝一个全新的对象来实现:

代码语言:javascript
复制
...

$carB = clone $carA;
$carB->brand = '宝马';

var_dump($carA);
var_dump($carB);

上述代码的打印结果如下:

说明 carB 确实和 carA 已经完全独立了,属性值的修改互不影响,但果真如此吗?我们增加点复杂度,现在在对象上新增对象属性:

代码语言:javascript
复制
<?php
$engine = new stdClass();
$engine->num = 4;

$carA = new stdClass();
$carA->brand = '奔驰';
$carA->power = '汽油';
$carA->engine = $engine;

$carB = clone $carA;
$carB->brand = '领克02';
$carB->power = '电池';
$carB->engine->num = 3;

var_dump($carA);
var_dump($carB);

再次执行上述代码,打印结果如下:

又出幺蛾子了!这个时候,你会发现虽然通过 clone 拷贝的对象普通属性不再相互污染,但是嵌套的对象属性依然存在这个互相影响的问题,因此,我们把引用赋值和 clone 拷贝统统称之为「浅拷贝」,只有嵌套的对象属性也不相互污染的拷贝才是真正相互对立的「深拷贝」。要实现这种深拷贝,就要用到我们前面提到的 __clone 魔术方法。

但是 stdClass 显然也不支持这种类方法,因此,需要鸟枪换炮,换成真正的类来演示:

代码语言:javascript
复制
<?php

class Engine
{
    public $num = 4;
}

class Car
{
    public $brand;
    public $power;
    /**
     * @var Engine
     */
    public $engine;

    public function __clone()
    {
        $this->engine = clone $this->engine;
    }
}

$benz = new Car();
$benz->brand = '奔驰';
$benz->power = '汽油';
$engine = new Engine();
$benz->engine = $engine;

$lnykco02 = clone $benz;
$lnykco02->brand = '领克02';
$lnykco02->power = '电池';
$lnykco02->engine->num = 3;

var_dump($benz);
var_dump($lnykco02);

可以看到,我们在 __clone 方法中所做的也很简单,无非是将对象属性再做一次 clone 拷贝而已,这样一来,再次执行上述代码,打印结果如下:

可以看到,无论是普通属性,还是嵌套对象属性,都已经完全独立,不再相互干扰,从而实现了真正意义上的深拷贝。

关于魔术方法,学院君就简单介绍到这里,下篇教程,我们将简单探讨下 PHP 中的异常处理逻辑,并以此作为面向对象编程的终结篇。

(全文完)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 极客书房 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、概述
  • 2、__sleep()、__wakeup() 与对象序列化
    • 反序列化实现原理
    • 3、__call() 和 __callStatic()
    • 4、__set()、__get()、__isset() 和 __unset()
    • 5、__invoke()
    • 6、__clone() 与对象复制
    相关产品与服务
    文件存储
    文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档