魔术方法

魔术方法是特殊方法,它们覆盖 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

class 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(): array

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

示例 #7 使用 __debugInfo()

<?php
class 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
15 年前
__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
15 年前
请务必在从使用它的父类继承的类中定义 __set_state(),因为静态 __set_state() 调用将被调用以用于任何子类。如果不仔细操作,您最终会得到错误类型的对象。以下是一个示例

<?php
class A
{
public
$var1;

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

class
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
class 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
)
$
ctamayo at sitecrafting dot com
3 年前
由于 PHP <= 7.3 中的一个错误,从 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) {
// }
// }

?>

错误报告:https://bugs.php.net/bug.php?id=69264
daniel dot peder at gmail dot com
6 年前
http://sandbox.onlinephpfunctions.com/code/4d2cc3648aed58c0dad90c7868173a4775e5ba0c

恕我直言,这是一个错误,或者需要更改功能

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

PS:试图提交错误,但如果没有补丁,错误就会被忽略,不幸的是,我不会 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
"with type hinting: ";
echo (
$demo_name === $all_products[(string)$pid]) ? "ok" : "fail";
echo
"\n";

echo
"without type hinting: ";
echo (
$demo_name === $all_products[$pid]) ? "ok" : "fail";
echo
"\n";
martin dot goldinger at netserver dot ch
18 年前
当您使用会话时,保持会话数据量小非常重要,因为 unserialize 的性能较低。每个类都应该继承自这个类。结果是,不会将空值写入会话数据。这将提高性能。

<?
class BaseObject
{
function __sleep()
{
$vars = (array)$this;
foreach ($vars as $key => $val)
{
if (is_null($val))
{
unset($vars[$key]);
}
}
return array_keys($vars);
}
};
?>
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 to Function with call_user_func_array failed", E_USER_ERROR);

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

function
__construct($callerClassName = false, $callerModuleName = 'Webboard') {
if (
$callerClassName == false)
trigger_error('No Classname', E_USER_ERROR);

$this->module = $callerModuleName;

if (
class_exists($callerClassName))
$this->caller = new $callerClassName();
else
trigger_error('Class not exists: \''.$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('Caller is no object!', 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('Caller is no object!', 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('Caller is no object!', E_USER_ERROR);
$this->unsetModuleInformation();
}

function
__set($set, $val) {
$this->setModuleInformation();
if (
is_object($this->caller))
$this->caller->{$set} = $val;
else
trigger_error('Caller is no object!', E_USER_ERROR);
$this->unsetModuleInformation();
}

function
__get($get) {
$this->setModuleInformation();
if (
is_object($this->caller)) {
if (isset(
$
Outputs something Like

Constructor will have no Module Information... Use __init() instead!
--> <--

Using of __init()!
--> Guestbook <--

123

456

789

Guestbook
jeffxlevy at gmail dot com
19 years ago
Intriguing what happens when __sleep() and __wakeup() and sessions() are mixed. I had a hunch that, as session data is serialized, __sleep would be called when an object, or whatever, is stored in _SESSION. true. The same hunch applied when session_start() was called. Would __wakeup() be called? True. Very helpful, specifically as I'm building massive objects (well, lots of simple objects stored in sessions), and need lots of automated tasks (potentially) reloaded at "wakeup" time. (for instance, restarting a database session/connection).
ddavenport at newagedigital dot com
19 years ago
面向对象编程 (OOP) 的原则之一是封装,即对象应该处理自己的数据,而不是其他对象的数据。要求基类负责子类的​​数据,尤其是考虑到一个类可能无法知道它会被扩展多少种方式,这是不负责任和危险的。

考虑以下示例...

<?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... }
}

?>

派生类对自己的数据有更好的控制,我们不必担心存储不应该存储的东西。
To Top