PHP Conference Japan 2024

魔術方法

魔術方法是在對物件執行特定動作時,覆寫 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

這個 static 方法會在類別被 var_export() 匯出時呼叫。

這個方法唯一的參數是一個陣列,包含了以 ['屬性' => 值, ...] 形式匯出的屬性。

範例 #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(),因此重新匯入物件將會導致 Error 例外,如果沒有實作 __set_state() 的話。這特別會影響到一些內部的類別。 程式設計師有責任去驗證只有類別有實作 __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 筆記

49
jon at webignition dot net
16 年前
__toString() 方法對於將類別屬性名稱和值轉換為常見的資料字串表示形式(有很多種選擇)非常有用。我提到這一點是因為先前對 __toString() 的參考僅指除錯用途。

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

- 將資料持有物件表示為
- XML
- 原始 POST 資料
- GET 查詢字串
- header name:value 配對

- 將自訂郵件物件表示為實際電子郵件(標頭然後是主體,都正確表示)

在建立類別時,請考慮有哪些可能的標準字串表示形式,以及其中哪些與類別的目的最相關。

能夠以標準化字串形式表示資料持有物件,可以更輕鬆地以可互通的方式與其他應用程式共享您的內部資料表示形式。
8
tyler at nighthound dot us
1 年前
請注意,在 PHP 8.2 中,實作 __serialize() 並不會控制 json_encode() 的輸出。您仍然必須實作 JsonSerializable。
18
jsnell at e-normous dot com
16 年前
請非常小心在繼承自使用 __set_state() 的父類別的類別中定義 __set_state(),因為 static __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)
}
*/
?>
11
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
)
$
8
martin dot goldinger at netserver dot ch
19 年前
當你使用 session 時,由於 `unserialize` 的效能較低,保持 session 資料量小非常重要。每個類別都應該繼承自這個類別。結果將會是,沒有 `null` 值會被寫入 session 資料中。這會提升效能。

<?
class BaseObject
{
function __sleep()
{
$vars = (array)$this;
foreach ($vars as $key => $val)
{
if (is_null($val))
{
unset($vars[$key]);
}
}
return array_keys($vars);
}
};
?>
7
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";
5
ctamayo at sitecrafting dot com
4 年前
由於 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
5
jeffxlevy at gmail dot com
19 年前
當 `__sleep()` 和 `__wakeup()` 與 `sessions()` 混合使用時,會發生什麼事,這很有趣。我有一個預感,當物件或任何東西被儲存在 `_SESSION` 中時,由於 session 資料被序列化,`__sleep` 會被呼叫。沒錯。當呼叫 `session_start()` 時,也有同樣的預感。`__wakeup()` 會被呼叫嗎?沒錯。這非常有幫助,特別是因為我正在建構龐大的物件(嗯,很多簡單的物件被儲存在 session 中),並且需要很多自動化的任務在「喚醒」時重新載入。(例如,重新啟動資料庫 session/連線)。
5
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... }
}

?>

衍生類別可以更好地控制其資料,而且我們不必擔心會儲存不應該儲存的東西。
4
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(
$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>');
?>

輸出類似以下結果

建構子不會有模組資訊... 請改用 __init()!
--> <--

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

123

456

789

Guestbook
To Top