2024年PHP日本大会

魔术方法

魔术方法是特殊的方法,当对对象执行某些操作时,它们会覆盖PHP的默认操作。

注意

所有以__开头的函数名都被PHP保留。因此,除非要覆盖PHP的行为,否则不建议使用此类函数名。

以下函数名被认为是魔术方法:__construct()__destruct()__call()__callStatic()__get()__set()__isset()__unset()__sleep()__wakeup()__serialize()__unserialize()__toString()__invoke()__set_state()__clone()__debugInfo()

警告

__construct()__destruct()__clone()之外的所有魔术方法 *必须* 声明为 public,否则会发出 E_WARNING 警告。在PHP 8.0.0之前,对于魔术方法 __sleep()__wakeup()__serialize()__unserialize()__set_state() 不会发出任何诊断信息。

警告

如果在魔术方法的定义中使用了类型声明,则它们必须与本文档中描述的签名相同。否则,会发出致命错误。在PHP 8.0.0之前,不会发出任何诊断信息。但是,__construct()__destruct() 不能声明返回类型;否则会发出致命错误。

__sleep()__wakeup()

public __sleep(): array
public __wakeup(): void

serialize() 检查类是否具有名为 __sleep() 的魔术方法。如果存在,则在任何序列化之前都会执行该函数。它可以清理对象,并应返回一个数组,其中包含应序列化的该对象所有变量的名称。如果该方法没有返回任何内容,则会序列化 null 并发出 E_NOTICE 通知。

注意:

__sleep() 无法返回父类中私有属性的名称。这样做会导致 E_NOTICE 级别的错误。请改用 __serialize()

注意:

从PHP 8.0.0开始,从__sleep()返回非数组的值会生成警告。以前,它会生成通知。

__sleep() 的预期用途是提交挂起的数据或执行类似的清理任务。此外,如果非常大的对象不需要完全保存,则该函数非常有用。

相反,unserialize() 检查是否存在名为 __wakeup() 的魔术方法。如果存在,此函数可以重建对象可能拥有的任何资源。

__wakeup() 的预期用途是重新建立在序列化期间可能丢失的任何数据库连接并执行其他重新初始化任务。

示例 #1 休眠和唤醒

<?php
class Connection
{
protected
$link;
private
$dsn, $username, $password;

public function
__construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}

private function
connect()
{
$this->link = new PDO($this->dsn, $this->username, $this->password);
}

public function
__sleep()
{
return array(
'dsn', 'username', 'password');
}

public function
__wakeup()
{
$this->connect();
}
}
?>

__serialize()__unserialize()

public __serialize(): array
public __unserialize(array $data): void

serialize() 检查类是否具有名为 __serialize() 的魔术方法。如果存在,则在任何序列化之前都会执行该函数。它必须构造并返回一个键值对的关联数组,这些键值对表示对象的序列化形式。如果未返回数组,则会抛出 TypeError 异常。

注意:

如果 __serialize()__sleep() 都在同一个对象中定义,则只调用 __serialize()__sleep() 将被忽略。如果对象实现了 Serializable 接口,则接口的 serialize() 方法将被忽略,并改用 __serialize()

__serialize() 的预期用途是定义对象的序列化友好的任意表示形式。数组的元素可能对应于对象的属性,但这并不是必需的。

相反,unserialize() 检查是否存在名为 __unserialize() 的魔术方法。如果存在,此函数将接收从 __serialize() 返回的已恢复数组。然后,它可以根据需要从该数组中恢复对象的属性。

注意:

如果 __unserialize()__wakeup() 都在同一个对象中定义,则只调用 __unserialize()__wakeup() 将被忽略。

注意:

此功能从 PHP 7.4.0 开始可用。

示例 #2 序列化和反序列化

<?php
class Connection
{
protected
$link;
private
$dsn, $username, $password;

public function
__construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}

private function
connect()
{
$this->link = new PDO($this->dsn, $this->username, $this->password);
}

public function
__serialize(): array
{
return [
'dsn' => $this->dsn,
'user' => $this->username,
'pass' => $this->password,
];
}

public function
__unserialize(array $data): void
{
$this->dsn = $data['dsn'];
$this->username = $data['user'];
$this->password = $data['pass'];

$this->connect();
}
}
?>

__toString()

public __toString(): string

__toString() 方法允许类决定当它被当作字符串处理时如何反应。例如,echo $obj; 将打印什么。

警告

从 PHP 8.0.0 开始,返回值遵循标准的 PHP 类型语义,这意味着如果可能并且禁用了严格类型,它将被强制转换为string

如果启用了严格类型Stringable 对象将*不会*被string 类型声明接受。如果需要这种行为,类型声明必须通过联合类型接受Stringablestring

从 PHP 8.0.0 开始,任何包含 __toString() 方法的类也将隐式实现 Stringable 接口,因此将通过该接口的类型检查。尽管如此,建议显式实现该接口。

在 PHP 7.4 中,返回值*必须*是 string,否则将抛出 Error

在 PHP 7.4.0 之前,返回值*必须*是 string,否则将发出致命的 E_RECOVERABLE_ERROR

警告

在 PHP 7.4.0 之前,无法从 __toString() 方法中抛出异常。这样做会导致致命错误。

示例 #3 简单示例

<?php
//声明一个简单的类
class TestClass
{
public
$foo;

public function
__construct($foo)
{
$this->foo = $foo;
}

public function
__toString()
{
return
$this->foo;
}
}

$class = new TestClass('Hello');
echo
$class;
?>

以上示例将输出

Hello

__invoke()

__invoke( ...$values): mixed

当脚本尝试将对象作为函数调用时,将调用 __invoke() 方法。

示例 #4 使用 __invoke()

<?php
class CallableClass
{
public function
__invoke($x)
{
var_dump($x);
}
}
$obj = new CallableClass;
$obj(5);
var_dump(is_callable($obj));
?>

以上示例将输出

int(5)
bool(true)

示例 #5 使用 __invoke()

<?php
class Sort
{
private
$key;

public function
__construct(string $key)
{
$this->key = $key;
}

public function
__invoke(array $a, array $b): int
{
return
$a[$this->key] <=> $b[$this->key];
}
}

$customers = [
[
'id' => 1, 'first_name' => 'John', 'last_name' => 'Do'],
[
'id' => 3, 'first_name' => 'Alice', 'last_name' => 'Gustav'],
[
'id' => 2, 'first_name' => 'Bob', 'last_name' => 'Filipe']
];

//按名字排序客户
usort($customers, new Sort('first_name'));
print_r($customers);

//按姓氏排序客户
usort($customers, new Sort('last_name'));
print_r($customers);
?>

以上示例将输出

Array
(
    [0] => Array
        (
            [id] => 3
            [first_name] => Alice
            [last_name] => Gustav
        )

    [1] => Array
        (
            [id] => 2
            [first_name] => Bob
            [last_name] => Filipe
        )

    [2] => Array
        (
            [id] => 1
            [first_name] => John
            [last_name] => Do
        )

)
Array
(
    [0] => Array
        (
            [id] => 1
            [first_name] => John
            [last_name] => Do
        )

    [1] => Array
        (
            [id] => 2
            [first_name] => Bob
            [last_name] => Filipe
        )

    [2] => Array
        (
            [id] => 3
            [first_name] => Alice
            [last_name] => Gustav
        )

)

__set_state()

static __set_state(array $properties): object

这个静态方法被 var_export()导出的类调用。

此方法的唯一参数是一个数组,其中包含以 ['property' => value, ...] 形式导出的属性。

示例 #6 使用 __set_state()

<?php

A
{
public
$var1;
public
$var2;

public static function
__set_state($an_array)
{
$obj = new A;
$obj->var1 = $an_array['var1'];
$obj->var2 = $an_array['var2'];
return
$obj;
}
}

$a = new A;
$a->var1 = 5;
$a->var2 = 'foo';

$b = var_export($a, true);
var_dump($b);
eval(
'$c = ' . $b . ';');
var_dump($c);
?>

以上示例将输出

string(60) "A::__set_state(array(
   'var1' => 5,
   'var2' => 'foo',
))"
object(A)#2 (2) {
  ["var1"]=>
  int(5)
  ["var2"]=>
  string(3) "foo"
}

注意导出对象时,var_export() 不会检查对象的类是否实现了 __set_state(),因此如果未实现 __set_state(),则重新导入对象将导致 Error 异常。尤其是在某些内部类中,这种情况会受到影响。 程序员有责任验证只有那些类实现了 __set_state() 的对象才会被重新导入。

__debugInfo()

__debugInfo(): 数组

此方法由 var_dump() 在转储对象时调用,以获取应显示的属性。如果对象上未定义此方法,则将显示所有公共、受保护和私有属性。

示例 #7 使用 __debugInfo()

<?php
C {
private
$prop;

public function
__construct($val) {
$this->prop = $val;
}

public function
__debugInfo() {
return [
'propSquared' => $this->prop ** 2,
];
}
}

var_dump(new C(42));
?>

以上示例将输出

object(C)#1 (1) {
  ["propSquared"]=>
  int(1764)
}
添加笔记

用户贡献笔记 10 条笔记

jon at webignition dot net
16 年前
__toString() 方法对于将类属性名称和值转换为常见的数据字符串表示非常有用(有很多选择)。我提到这一点是因为以前对 __toString() 的引用仅指调试用途。

我以前曾以下列方式使用 __toString() 方法

- 将数据持有对象表示为
- XML
- 原始 POST 数据
- GET 查询字符串
- 头部名称:值对

- 将自定义邮件对象表示为实际邮件(头部然后是正文,所有内容都正确表示)

创建类时,请考虑可用的可能的标准字符串表示形式,以及其中哪些与类的用途最相关。

能够以标准化的字符串形式表示数据持有对象,使得内部数据表示更容易以可互操作的方式与其他应用程序共享。
tyler at nighthound dot us
1 年前
请注意,从 PHP 8.2 开始,实现 __serialize() 无法控制 json_encode() 的输出。您仍然必须实现 JsonSerializable。
jsnell at e-normous dot com
16 年前
在从使用它的父类继承的类中定义 __set_state() 时要非常小心,因为静态 __set_state() 调用将对任何子类调用。如果不小心,最终会得到错误类型的对象。这是一个例子

<?php
A
{
public
$var1;

public static function
__set_state($an_array)
{
$obj = new A;
$obj->var1 = $an_array['var1'];
return
$obj;
}
}

B extends A {
}

$b = new B;
$b->var1 = 5;

eval(
'$new_b = ' . var_export($b, true) . ';');
var_dump($new_b);
/*
object(A)#2 (1) {
["var1"]=>
int(5)
}
*/
?>
kguest at php dot net
7 年前
在对对象调用 print_r 时,也会使用 __debugInfo

$ cat test.php
<?php
FooQ {

private
$bar = '';

public function
__construct($val) {

$this->bar = $val;
}

public function
__debugInfo()
{
return [
'_bar' => $this->bar];
}
}
$fooq = new FooQ("q");
print_r ($fooq);

$
php test.php
FooQ Object
(
[
_bar] => q
)
$
martin dot goldinger at netserver dot ch
19 年前
当您使用会话时,由于 unserialize 性能低下,保持会话数据较小非常重要。每个类都应该从此类扩展。结果是,不会将空值写入会话数据。它将提高性能。

<?
类 BaseObject
{
函数 __sleep()
{
$vars = (array)$this;
foreach ($vars as $key => $val)
{
if (is_null($val))
{
unset($vars[$key]);
}
}
return array_keys($vars);
}
};
?>
daniel dot peder at gmail dot com
6 年前
http://sandbox.onlinephpfunctions.com/code/4d2cc3648aed58c0dad90c7868173a4775e5ba0c

我认为是一个错误或需要更改功能

提供对象作为数组索引不会尝试使用 __toString() 方法,因此使用一些易失性对象标识符来索引数组,这会破坏任何持久性。类型提示解决了这个问题,但是除了“字符串”类型提示之外,其他类型提示都不能在对象上工作,因此自动转换为字符串应该非常直观。

附注:尝试提交错误,但没有补丁,错误被忽略了,不幸的是,我不懂 C 语言编程

<?php

class shop_product_id {

protected
$shop_name;
protected
$product_id;

function
__construct($shop_name,$product_id){
$this->shop_name = $shop_name;
$this->product_id = $product_id;
}

function
__toString(){
return
$this->shop_name . ':' . $this->product_id;
}
}

$shop_name = 'Shop_A';
$product_id = 123;
$demo_id = $shop_name . ':' . $product_id;
$demo_name = 'Some product in shop A';

$all_products = [ $demo_id => $demo_name ];
$pid = new shop_product_id( $shop_name, $product_id );

echo
"使用类型提示: ";
echo (
$demo_name === $all_products[(string)$pid]) ? "ok" : "fail";
echo
"\n";

echo
"不使用类型提示: ";
echo (
$demo_name === $all_products[$pid]) ? "ok" : "fail";
echo
"\n";
ctamayo at sitecrafting dot com
4年前
由于PHP <= 7.3中的一个bug,从SPL类覆盖__debugInfo()方法会被静默忽略。

<?php

class Debuggable extends ArrayObject {
public function
__debugInfo() {
return [
'special' => 'This should show up'];
}
}

var_dump(new Debuggable());

// 预期输出:
// object(Debuggable)#1 (1) {
// ["special"]=>
// string(19) "This should show up"
// }

// 实际输出:
// object(Debuggable)#1 (1) {
// ["storage":"ArrayObject":private]=>
// array(0) {
// }
// }

?>

Bug报告: https://bugs.php.net/bug.php?id=69264
jeffxlevy at gmail dot com
19 年前
当__sleep()、__wakeup()和session()混合使用时,会发生有趣的事情。我有一种预感,由于session数据是序列化的,所以当对象(或任何东西)存储在_SESSION中时,会调用__sleep()。这是正确的。当调用session_start()时,同样的预感也适用。__wakeup()会被调用吗?是的。这非常有用,特别是当我构建大型对象(或者说,许多简单的对象存储在session中),并且需要在“唤醒”时重新加载许多自动化任务(可能)。(例如,重新启动数据库会话/连接)。
ddavenport at newagedigital dot com
19 年前
面向对象编程的一个原则就是封装——一个对象应该处理它自己的数据,而不是其他对象的数据。要求基类处理子类的数 据,尤其考虑到一个类不可能知道它会被扩展多少种不同的方式,这是不负责任和危险的。

考虑以下情况……

<?php
class SomeStupidStorageClass
{
public function
getContents($pos, $len) { ...stuff... }
}

class
CryptedStorageClass extends SomeStupidStorageClass
{
private
$decrypted_block;
public function
getContents($pos, $len) { ...decrypt... }
}
?>

如果SomeStupidStorageClass决定序列化其子类的数 据以及它自己的数据,那么曾经加密的东西的一部分可能会以明文形式存储在该东西存储的任何地方。显然,CryptedStorageClass永远不会选择这样做……但它必须要么知道如何在不调用parent::_sleep()的情况下序列化其父类的数据,要么让基类做它想做的事情。

再次考虑封装,任何类都不必知道父类如何处理它自己的私有数据。它当然也不必担心用户会为了方便而找到破坏访问控制的方法。

如果一个类既想要私有/受保护的数据,又想在序列化后继续存在,它应该有自己的__sleep()方法,该方法要求父类报告它自己的字段,然后根据需要添加到列表中。就像这样……

<?php

class BetterClass
{
private
$content;

public function
__sleep()
{
return array(
'basedata1', 'basedata2');
}

public function
getContents() { ...stuff... }
}

class
BetterDerivedClass extends BetterClass
{
private
$decrypted_block;

public function
__sleep()
{
return
parent::__sleep();
}

public function
getContents() { ...decrypt... }
}

?>

派生类对自己的数据有更好的控制,我们不必担心存储不应该存储的东西。
rayRO
18年前
如果你使用魔术方法`__set()`,请确保调用
<?php
$myobject
->test['myarray'] = 'data';
?>
不会出现!

如果你想使用`__set`方法,你必须这样做:
<?php
$myobject
->test = array('myarray' => 'data');
?>

如果一个变量已经被设置,`__set`魔术方法就不会出现了!

我的第一个解决方案是使用一个调用者类。
有了它,我总是知道我当前使用哪个模块!
但是谁需要它呢……:]
还有更好的解决方案……
这是代码

<?php
class Caller {
public
$caller;
public
$module;

function
__call($funcname, $args = array()) {
$this->setModuleInformation();

if (
is_object($this->caller) && function_exists('call_user_func_array'))
$return = call_user_func_array(array(&$this->caller, $funcname), $args);
else
trigger_error("调用函数时 call_user_func_array 失败", E_USER_ERROR);

$this->unsetModuleInformation();
return
$return;
}

function
__construct($callerClassName = false, $callerModuleName = 'Webboard') {
if (
$callerClassName == false)
trigger_error('未指定类名', E_USER_ERROR);

$this->module = $callerModuleName;

if (
class_exists($callerClassName))
$this->caller = new $callerClassName();
else
trigger_error('类不存在: \''.$callerClassName.'\'', E_USER_ERROR);

if (
is_object($this->caller))
{
$this->setModuleInformation();
if (
method_exists($this->caller, '__init'))
$this->caller->__init();
$this->unsetModuleInformation();
}
else
trigger_error('调用者不是对象!', E_USER_ERROR);
}

function
__destruct() {
$this->setModuleInformation();
if (
method_exists($this->caller, '__deinit'))
$this->caller->__deinit();
$this->unsetModuleInformation();
}

function
__isset($isset) {
$this->setModuleInformation();
if (
is_object($this->caller))
$return = isset($this->caller->{$isset});
else
trigger_error('调用者不是对象!', E_USER_ERROR);
$this->unsetModuleInformation();
return
$return;
}

function
__unset($unset) {
$this->setModuleInformation();
if (
is_object($this->caller)) {
if (isset(
$this->caller->{$unset}))
unset(
$this->caller->{$unset});
}
else
trigger_error('调用者不是对象!', E_USER_ERROR);
$this->unsetModuleInformation();
}

function
__set($set, $val) {
$this->setModuleInformation();
if (
is_object($this->caller))
$this->caller->{$set} = $val;
else
trigger_error('调用者不是对象!', E_USER_ERROR);
$this->unsetModuleInformation();
}

function
__get($get) {
$this->setModuleInformation();
if (
is_object($this->caller)) {
if (isset(
$this->caller->{$get}))
$return = $this->caller->{$get};
else
$return = false;
}
else
trigger_error('调用者不是对象!', E_USER_ERROR);
$this->unsetModuleInformation();
return
$return;
}

function
setModuleInformation() {
$this->caller->module = $this->module;
}

function
unsetModuleInformation() {
$this->caller->module = NULL;
}
}

// 这可以是一个配置类?
class Config {
public
$module;

public
$test;

function
__construct()
{
print(
'构造函数不会包含模块信息... 请使用 __init() 代替!<br />');
print(
'--> '.print_r($this->module, 1).' <--');
print(
'<br />');
print(
'<br />');
$this->test = '123';
}

function
__init()
{
print(
'使用 __init()!<br />');
print(
'--> '.print_r($this->module, 1).' <--');
print(
'<br />');
print(
'<br />');
}

function
testFunction($test = false)
{
if (
$test != false)
$this->test = $test;
}
}

echo(
'<pre>');
$wow = new Caller('Config', 'Guestbook');
print_r($wow->test);
print(
'<br />');
print(
'<br />');
$wow->test = '456';
print_r($wow->test);
print(
'<br />');
print(
'<br />');
$wow->testFunction('789');
print_r($wow->test);
print(
'<br />');
print(
'<br />');
print_r($wow->module);
echo(
'</pre>');
?>

输出类似如下内容

构造函数不会包含模块信息... 请使用 __init() 代替!
--> <--

使用 __init()!
--> Guestbook <--

123

456

789

Guestbook
To Top