魔术方法
魔术方法是一种特殊的方法,当对对象执行某些操作时会覆盖 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()
serialize() 函数会检查类中是否存在一个魔术方法
__sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。
如果该方法未返回任何内容,则 null
被序列化,并产生一个 E_NOTICE
级别的错误。
注意:
__sleep() 不能返回父类的私有成员的名字。这样做会产生一个
E_NOTICE
级别的错误。使用 __serialize() 接口替代。
注意:
自 PHP 8.0.0 起,__sleep() 的返回值不是数组会生成 warning。之前生成 notice。
__sleep() 方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。
与之相反,unserialize() 会检查是否存在一个 __wakeup()
方法。如果存在,则会先调用 __wakeup
方法,预先准备对象需要的资源。
__wakeup() 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。
示例 #1 Sleep 和 wakeup
<?php
class Connection
{
protected $link;
private $server, $username, $password, $db;
public function __construct($server, $username, $password, $db)
{
$this->server = $server;
$this->username = $username;
$this->password = $password;
$this->db = $db;
$this->connect();
}
private function connect()
{
$this->link = mysql_connect($this->server, $this->username, $this->password);
mysql_select_db($this->db, $this->link);
}
public function __sleep()
{
return array('server', 'username', 'password', 'db');
}
public function __wakeup()
{
$this->connect();
}
}
?>
__serialize() 和 __unserialize()
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()
__toString() 方法用于一个类被当成字符串时应怎样回应。例如
echo $obj;
应该显示些什么。
从 PHP 8.0.0 起,返回值遵循标准的 PHP 类型语义, 这意味着如果禁用 严格类型 ,它将会强制转换为字符串。
如果启用严格类型,则声明的 string 类型将不会接受 Stringable 对象。如果需要这种行为,类型声明必须接受 Stringable 和 string 的联合类型。
从 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() 方法会被自动调用。
示例 #4 使用 __invoke()
<?php
class CallableClass
{
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']
];
// sort customers by first name
usort($customers, new Sort('first_name'));
print_r($customers);
// sort customers by last name
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()
当调用 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);
var_dump($b);
?>
以上示例会输出:
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()
当通过 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 notes
The __toString() method is extremely useful for converting class attribute names and values into common string representations of data (of which there are many choices). I mention this as previous references to __toString() refer only to debugging uses.
I have previously used the __toString() method in the following ways:
- representing a data-holding object as:
- XML
- raw POST data
- a GET query string
- header name:value pairs
- representing a custom mail object as an actual email (headers then body, all correctly represented)
When creating a class, consider what possible standard string representations are available and, of those, which would be the most relevant with respect to the purpose of the class.
Being able to represent data-holding objects in standardised string forms makes it much easier for your internal representations of data to be shared in an interoperable way with other applications.
Please note that as of PHP 8.2 implementing __serialize() has no control over the output of json_encode(). you still have to implement JsonSerializable.
Be very careful to define __set_state() in classes which inherit from a parent using it, as the static __set_state() call will be called for any children. If you are not careful, you will end up with an object of the wrong type. Here is an example:
<?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)
}
*/
?>
__debugInfo is also utilised when calling print_r on an object:
$ 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
)
$
When you use sessions, its very important to keep the sessiondata small, due to low performance with unserialize. Every class shoud extend from this class. The result will be, that no null Values are written to the sessiondata. It will increase performance.
<?
class BaseObject
{
function __sleep()
{
$vars = (array)$this;
foreach ($vars as $key => $val)
{
if (is_null($val))
{
unset($vars[$key]);
}
}
return array_keys($vars);
}
};
?>
http://sandbox.onlinephpfunctions.com/code/4d2cc3648aed58c0dad90c7868173a4775e5ba0c
IMHO a bug or need feature change
providing a object as a array index doesn't try to us __toString() method so some volatile object identifier is used to index the array, which is breaking any persistency. Type hinting solves that, but while other than "string" type hinting doesn't work on ob jects, the automatic conversion to string should be very intuitive.
PS: tried to submit bug, but withot patch the bugs are ignored, unfortunately, I don't C coding
<?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";
Due to a bug in PHP <= 7.3, overriding the __debugInfo() method from SPL classes is silently ignored.
<?php
class Debuggable extends ArrayObject {
public function __debugInfo() {
return ['special' => 'This should show up'];
}
}
var_dump(new Debuggable());
// Expected output:
// object(Debuggable)#1 (1) {
// ["special"]=>
// string(19) "This should show up"
// }
// Actual output:
// object(Debuggable)#1 (1) {
// ["storage":"ArrayObject":private]=>
// array(0) {
// }
// }
?>
Bug report: https://bugs.php.net/bug.php?id=69264
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).
One of the principles of OOP is encapsulation--the idea that an object should handle its own data and no others'. Asking base classes to take care of subclasses' data, esp considering that a class can't possibly know how many dozens of ways it will be extended, is irresponsible and dangerous.
Consider the following...
<?php
class SomeStupidStorageClass
{
public function getContents($pos, $len) { ...stuff... }
}
class CryptedStorageClass extends SomeStupidStorageClass
{
private $decrypted_block;
public function getContents($pos, $len) { ...decrypt... }
}
?>
If SomeStupidStorageClass decided to serialize its subclasses' data as well as its own, a portion of what was once an encrypted thingie could be stored, in the clear, wherever the thingie was stored. Obviously, CryptedStorageClass would never have chosen this...but it had to either know how to serialize its parent class's data without calling parent::_sleep(), or let the base class do what it wanted to.
Considering encapsulation again, no class should have to know how the parent handles its own private data. And it certainly shouldn't have to worry that users will find a way to break access controls in the name of convenience.
If a class wants both to have private/protected data and to survive serialization, it should have its own __sleep() method which asks the parent to report its own fields and then adds to the list if applicable. Like so....
<?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... }
}
?>
The derived class has better control over its data, and we don't have to worry about something being stored that shouldn't be.
If you use the Magical Method '__set()', be shure that the call of
<?php
$myobject->test['myarray'] = 'data';
?>
will not appear!
For that u have to do it the fine way if you want to use __set Method ;)
<?php
$myobject->test = array('myarray' => 'data');
?>
If a Variable is already set, the __set Magic Method already wont appear!
My first solution was to use a Caller Class.
With that, i ever knew which Module i currently use!
But who needs it... :]
There are quiet better solutions for this...
Here's the Code:
<?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($this->caller->{$get}))
$return = $this->caller->{$get};
else
$return = false;
}
else
trigger_error('Caller is no object!', E_USER_ERROR);
$this->unsetModuleInformation();
return $return;
}
function setModuleInformation() {
$this->caller->module = $this->module;
}
function unsetModuleInformation() {
$this->caller->module = NULL;
}
}
// Well this can be a Config Class?
class Config {
public $module;
public $test;
function __construct()
{
print('Constructor will have no Module Information... Use __init() instead!<br />');
print('--> '.print_r($this->module, 1).' <--');
print('<br />');
print('<br />');
$this->test = '123';
}
function __init()
{
print('Using of __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>');
?>
Outputs something Like:
Constructor will have no Module Information... Use __init() instead!
--> <--
Using of __init()!
--> Guestbook <--
123
456
789
Guestbook