类和对象:魔术方法

在之前的内容中,我们已经学完了类和对象的主要内容,其中包括类的基本概念与结构、三种特殊的类(抽象类、final 类和匿名类)以及接口和 Trait。建议再回过去看面向对象一节,结合接口与 Trait 可以更加理解面向对象的含义。

本节我们进入魔术方法的学习。

魔术方法自然不是创造魔术的方法,在 PHP 中,所有以双下划线__开头的类方法保留为魔术方法。目前 PHP 的魔术方法有以下几个:

方法名 描述
__construct() 构造函数,创建新对象时调用,适合用来初始化对象
__destruct() 析构函数,对象的所有引用被删除或者对象被显示销毁时调用
__get() 访问不可访问的属性时调用
__set() 在给不可访问属性赋值时调用
__call() 在对象中调用不可访问的方法时调用
__callStatic() 在试图调用一个不可访问的静态方法时调用
__clone() 如果定义了__clone()方法,使用clone()方法进行对象的克隆时会自动调用该方法,可用于修改属性的值
__invoke() 当把一个对象当作函数使用时自动调用该方法
__isset() 使用isset()empty()访问一个不可访问的类属性时调用该方法
__unset() 使用unset()访问一个不可访问的类属性时调用该方法
__set_state() 使用var_export()函数导出类时调用该静态方法
__toString() 当一个类被作为字符串输出时调用该方法
__debuginfo() 使用var_dump()函数输出一个对象时调用该方法
__sleep() 常用于序列化操作中,规定序列化一个对象时可返回的属性
__wakeup() 常用于反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。

__construct() 与 __destruct()

构造函数与析构函数在类和对象-类结构一节中已经详细说明,它们分别用于初始化对象和在对象被销毁或所有引用被删除时自动调用。

__get() 与 __set()

如上表的描述所言,__get()类方法用于在对象访问不允许被访问的属性时自动调用,__set()类方法则用于对象试图给不可访问属性赋值时自动调用。

我们也可以显式定义:

<?php

// 魔术方法

class Magic
{
    private $unable = '我是一个在类外部不可访问的属性';
    protected $inherit = '我是一个在类及子类内部可访问的属性';
    public $explicit = '噢~我是一个开放热情的属性,在哪都可以访问并修改';

    public function __construct()
    {
        echo '你构造了一个对象:',__CLASS__,"\n";
    }

    public function __get($name)
    {
        echo '你正在访问一个不存在或者没有权限访问的属性:',$name,"\n";
    }

    public function __set($name, $value)
    {
        echo '你正在试图将一个不存在或没有权限修改的属性(',$name,')修改为:',$value,"\n";
    }
}

// 测试:
$magic = new Magic();
echo $magic->unable;// 输出:你正在访问一个不存在或者没有权限访问的属性:unable
$magic->inherit = '我想修改这个属性的值';// 输出:你正在试图将一个不存在或没有权限修改的属性(inherit)修改为:我想修改这个属性的值

__call() 与 __callStatic()

这两个方法和__get()差不多,只是__get()是访问不可访问的属性,而__call()__callStatic()是访问不可访问的方法。从名称可以看出,__callStatic()是访问不可访问的静态方法时调用。这两个方法都需要显式定义,否则访问不可访问的方法或者静态方法时会报致命错误:

<?php

// 魔术方法

class Magic
{
    // ...类属性和构造函数

    public function __call($name, $arguments)
    {
        echo '你正在试图访问一个不可访问的方法(',$name,'),并且传入了参数:',implode(' ', $arguments), PHP_EOL;
    }

    static public function __callStatic($name, $arguments)
    {
        echo '你正在试图访问一个不可访问的静态方法(',$name,'),并且传入了参数:',implode(' ', $arguments), PHP_EOL;
    }
}

// 测试:
$magic = new Magic();
$magic->not_exit('参数1','参数2','参数3');// 输出:你正在试图访问一个不可访问的方法(not_exit),并且传入了参数:参数1 参数2 参数3
$magic::not_exit('参数1','参数2');// 输出:你正在试图访问一个不可访问的静态方法(not_exit),并且传入了参数:参数1 参数2

由上述代码可知,__callStatic()是一个静态方法,需要使用关键字static进行声明。

__clone()

__clone()类方法是使用方法clone()时的默认调用,意思是当使用clone()方法进行对象的克隆时会自动调用类的__clone()方法。

通常__clone()类方法用于修改新对象的属性值,运行下方示例代码,查看结果:

<?php

class Magic
{
    public $explicit = '噢~我是一个开放热情的属性,在哪都可以访问并修改';
    public function __clone()
    {
        $this->explicit = '我是被克隆之后的产物';// $this 指代克隆后的对象,而不是被克隆的原对象
        var_dump($this);
    }
}

// 测试:
$magic = new Magic();

// 与 $magic 对象一样
$magic2 = clone $magic;
/* 输出:
object(Magic)#2 (1) {
  ["explicit"]=>
  string(30) "我是被克隆之后的产物"
}
*/

// clone 对象所做的改变不影响原对象($magic)
$magic2->explicit = '我被一个clone的对象修改了值';
var_dump($magic);
/* 
object(Magic)#1 (1) {
  ["explicit"]=>
  string(70) "噢~我是一个开放热情的属性,在哪都可以访问并修改"
}
*/

// $magic3 所做的改变会影响到原对象($magic)
$magic3 = $magic;
$magic3->explicit = '使用等号进行的是对象的引用(而不是克隆),两个变量指向同一个对象';// 修改了属性值
var_dump($magic);
/* 输出:
object(Magic)#1 (1) {
  ["explicit"]=>
  string(96) "使用等号进行的是对象的引用(而不是克隆),两个变量指向同一个对象"
}
*/

PHP 默认的对象复制方法是引用复制(使用等号复制对象,二者是同一个引用),但clone()方法是复制原对象的当前状态(二者是不同的对象)。

__invoke()

当试图以调用函数的形式调用一个对象时,__invoke()被自动调用。如下示例:

<?php

// 魔术方法

class Magic
{
    // ...类属性和构造函数

    public function __invoke()
    {
        echo '你使用了错误的方式来调用函数', PHP_EOL;
    }
}

// 测试:
$magic = new Magic();
$magic();// 输出:你使用了错误的方式来调用函数

__isset() 与 __unset()

__isset()类方法是在对不可访问的类属性使用isset()函数时自动调用的。

isset()函数检测变量是否被定义(变量值存在且不为NULL),如果变量值存在且不为NULL就返回TRUE,否则返回FALSE。如下示例:

<?php

// 魔术方法

class Magic
{
    // ...类属性和构造函数

    public function __isset($name)
    {
        echo '你正在检测一个不可访问的类属性(',$name,')是否定义',"\n";
    }
}

// 测试:
$magic = new Magic();
$null = NULL;
var_dump(isset($null));// 输出:bool(false)
var_dump(isset($magic->unable));// 检测一个私有属性是否被定义。输出:bool(false)

__unset()类方法则是在对不可访问的类属性使用unset()函数时自动调用的。

unset()函数销毁指定的变量。如果在函数中使用,销毁的只是局部变量,如果需要销毁全局变量,则需要使用$_GLOBALS数组;如果销毁的是一个变量的引用,则只是销毁这个引用而不是原变量。如下示例:

<?php

// 魔术方法

class Magic
{
    // ...类属性和构造函数

    public function __unset($name)
    {
        echo '你正在销毁一个不可访问的类属性:',$name,"\n";
    }
}

// 测试:
$magic = new Magic();
$val = '极速教程';
$val2 = &$val;
unset($val2);
echo $val, PHP_EOL;// 输出:极速教程
unset($magic->unable);// 输出:你正在销毁一个不可访问的类属性:unable

__set_state()

该类方法为静态类,在使用函数var_export()时自动调用,该函数输出并返回变量的字符串表达。示例如下:

<?php

class Magic
{
    private $unable = '我是一个在类外部不可访问的属性';
    protected $inherit = '我是一个在类及子类内部可访问的属性';
    public $explicit = '噢~我是一个开放热情的属性,在哪都可以访问并修改';

    static public function __set_state($properties)
    {
        // $properties 是一个含有类属性默认值的键值对关联数组
        $obj = new Magic();

        // 默认值
        $obj->inherit = $properties['inherit'];

        // 可以给对象属性赋新值
        $obj->unable = '我在__set_state()静态函数中被赋予新的值';

        // 如果试图给未定义的类属性赋值,将会出错
        $obj->new = '新属性';

        return $obj;
    }
}

// 测试:
$magic = new Magic();
var_export($magic);
/* 输出:
Magic::__set_state(array(
   'unable' => '我是一个在类外部不可访问的属性',
   'inherit' => '我是一个在类及子类内部可访问的属性',
   'explicit' => '噢~我是一个开放热情的属性,在哪都可以访问并修改',
))
*/
eval('$export='.var_export($magic, true).';');
var_dump($export);
/* 输出:
object(Magic)#2 (4) {
  ["unable":"Magic":private]=>
  string(52) "我在__set_state()静态函数中被赋予新的值"
  ["inherit":protected]=>
  string(51) "我是一个在类及子类内部可访问的属性"
  ["explicit"]=>
  string(70) "噢~我是一个开放热情的属性,在哪都可以访问并修改"
  ["new"]=>
  string(9) "新属性"
}
*/

直接使用函数var_export()调用该对象所在类的静态方法__set_state()并输出其原型字符串,这里是:Magic::__set_state(array(...)),其中数组为类属性的集合。

当使用eval()函数将var_export()函数得到的结果拼接为一段可执行的代码(这里是:$export=var_export($magic, true);),然后输出该变量,可以发现在类静态方法__set_state()中设置的属性值生效了。

__toString()

有时候可能会将一个对象当作字符串变量输出,如果该对象所在类没有定义__toString()方法的话,会造成一个致命错误,如下:

<?php

class Magic {}

$magic = new Magic();
echo $magic;// 输出:Recoverable fatal error: Object of class Magic could not be converted to string

print($magic);// 输出:Recoverable fatal error: Object of class Magic could not be converted to string

printf('%s', $magic);// 输出:Recoverable fatal error: Object of class Magic could not be converted to string

为避免这种情况的发生,我们可以给出一个提示,在类中显式定义一个__toString()方法,提示该变量为对象而不是字符串,向类中添加方法:

    public function __toString()
    {
        $str = '您想要输出的变量是一个对象,而不是字符串!';
        return $str;
    }

再次执行输出对象操作:echo $magic;,将输出您想要输出的变量是一个对象,而不是字符串!

需要注意的是,__toString()方法必须返回一个字符串,否则将出错。

__debuginfo()

我们经常使用函数var_dump()来查看一个变量的属性和值,当使用该函数检查一个对象时,将输出该对象所在类的所有属性及默认值(无论其可见性是什么),一起来看看:

<?php

class Magic
{
    private $unable = '我是一个在类外部不可访问的属性';
    protected $inherit = '我是一个在类及子类内部可访问的属性';
    public $explicit = '噢~我是一个开放热情的属性,在哪都可以访问并修改';
}
$magic = new Magic();
var_dump($magic);
/* 输出:
object(Magic)#1 (3) {
  ["unable":"Magic":private]=>
  string(45) "我是一个在类外部不可访问的属性"
  ["inherit":protected]=>
  string(51) "我是一个在类及子类内部可访问的属性"
  ["explicit"]=>
  string(70) "噢~我是一个开放热情的属性,在哪都可以访问并修改"
}
*/

在类中定义__debuginfo()将帮助我们控制可输出的信息,例如:

<?php

class Magic
{
    private $unable = '我是一个在类外部不可访问的属性';
    protected $inherit = '我是一个在类及子类内部可访问的属性';
    public $explicit = '噢~我是一个开放热情的属性,在哪都可以访问并修改';

    public function __debugInfo()
    {
        return [
            '你想被看到的属性名'=>'和它的属性值',
            'explict'=>$this->explicit
        ];
    }
}
$magic = new Magic();
var_dump($magic);
/* 输出:
object(Magic)#1 (2) {
  ["你想被看到的属性名"]=>
  string(18) "和它的属性值"
  ["explict"]=>
  string(70) "噢~我是一个开放热情的属性,在哪都可以访问并修改"
}
*/

此时将输出你想看到的信息。注意:__debuginfo()方法必须返回一个数组。

__sleep() 与 __wakeup()

__sleep()方法用于清理对象。函数serialize()在执行序列化之前会先检查类中是否含有方法__sleep(),如果有,则先执行__sleep()方法。

注释掉__debuginfo()的定义,在类中添加如下代码:

    public function __sleep()
    {
        // 不能返回父类的私有成员的名字
        return ['explicit', 'unable'];
    }

然后进行测试:

$magic2 = new Magic();
$magic2->explicit = '试图修改属性';
$res2 = serialize($magic2);
echo $res2;// 输出:O:5:"Magic":2:{s:8:"explicit";s:18:"试图修改属性";s:13:" Magic unable";s:45:"我是一个在类外部不可访问的属性";}

可以看出__sleep()方法可以规定序列化一个对象时可返回的属性

__wakeup()方法则是在进行反序列化操作(函数unserialize())之前的预备工作,它准备反序列化这个对象所需要的资源


至此,PHP 魔术方法就结束了,许多魔术方法也不一定会用到,但对它们有一个大致的了解是很重要的。本节作为 PHP 类和对象章的最后一节,总共讲解了 15 个魔术方法的使用,实际运用时应该根据需求来定义或使用魔术方法。

同时,类和对象一章也到这里结束了。本部分概念与内容实例都较多,建议多尝试运行,并扩展其他例子以便熟悉使用。

下一章我们将进入命名空间的学习,我们也不用把所有代码都写在一个文件啦!