PHP 提供了兩種腳本/對象結束時的執行機制:
unset() 時觸發。exit() 時觸發。直覺上認為腳本級別應晚於對象級別,但實際測試卻呈現出極其反直覺、甚至看似「不可預測」的行為。本文通過層層遞進的實驗,還原真實執行順序,並給出最佳實踐建議。
<?php
register_shutdown_function(function(){ echo 'global
'; });
class A {
public function __destruct() { echo 'A::__destruct
'; }
}
new A;
?>
輸出:
A::__destruct
global
符合預期:對象先銷毀,腳本級回調最後執行。
<?php
class A {
public function __construct() {
register_shutdown_function(function(){ echo 'local
'; });
}
public function __destruct() { echo 'A::__destruct
'; }
}
new A;
?>
輸出:
local
A::__destruct
反轉! 對象內註冊的 shutdown 早於該對象的 destruct 執行。說明 shutdown 回調並非單純「腳本結束後」執行,其調度機制更為複雜。
<?php
register_shutdown_function(function(){ echo 'global
'; });
class A {
public function __construct() {
register_shutdown_function([$this, 'op']);
}
public function __destruct() { echo 'A::__destruct
'; }
public function op() { echo 'A::op
'; }
}
class B {
public function __construct() {
register_shutdown_function([$this, 'op']);
$obj = new A; // B 擁有 A 的引用
}
public function __destruct() { echo 'B::__destruct
'; }
public function op() { echo 'B::op
'; }
}
$b = new B;
?>
輸出:
global
B::op
A::op
A::__destruct
B::__destruct
觀察發現:
register_shutdown_function(無論全局還是對象內)全部早於任何 __destruct 執行。<?php
class A { public function __destruct() { echo 'A::__destruct
'; } }
class B {
public function __construct() { $obj = new A; }
public function __destruct() { echo 'B::__destruct
'; }
}
register_shutdown_function(function(){ echo 'global
'; });
$b = new B;
?>
輸出:
A::__destruct
global
B::__destruct
極其詭異! 全局 shutdown 竟然「插隊」在兩個對象的 destruct 之間。這說明:PHP 的關閉流程並非簡單的「先對象後腳本」,而是交織執行。
<?php
register_shutdown_function(function(){ echo 'global
'; });
class A {
public function __construct() {
register_shutdown_function([$this, 'op']);
exit; // 直接終止
}
public function __destruct() { echo 'A::__destruct
'; }
public function op() { echo 'A::op
'; }
}
class B {
public function __construct() {
register_shutdown_function([$this, 'op']);
$obj = new A; // 不會執行到這裡
}
public function __destruct() { echo 'B::__destruct
'; }
public function op() { echo 'B::op
'; }
}
$b = new B;
?>
輸出:
global
B::op
A::op
B::__destruct
A::__destruct
此時 B 的 destruct 反而早於 A,因為 A 的構造函數中 exit 導致 B 的構造未完成,$obj = new A 不會執行,B 不持有 A 的引用,故 B 先銷毀。
register_shutdown_function,再按引用關係逆序執行 __destruct。兩輪交織,而非簡單串行。register_shutdown_function,且不要在類構造函數中註冊(註冊時機不可控)。析構函數即使在使用
exit()終止腳本運行時也會被調用。在析構函數中調用exit()將會中止其餘關閉操作的運行。
這句話暗示了「關閉操作」包含 shutdown 回調與其他對象的 destruct,二者同屬關閉階段,順序由引擎內部調度,非開發者可控。