PHP中什么情况下不会调用__destruct函数?

62
class MyDestructableClass {
   function __construct() {
       print "\nIn constructor\n";
       $this->name = "MyDestructableClass";
   }

   function __destruct() {
       print "\nDestroying " . $this->name . "\n";
   }
}

$obj = new MyDestructableClass();

当上述脚本处于复杂的环境中时,exit时不会调用__destruct,但我无法轻易地重现它。有人注意到过吗? 编辑 我会在这里发布整个内容,这是Symfony的测试环境,如果你熟悉该框架,那么你可以很容易地重现它:
require_once dirname(__FILE__).'/../bootstrap/Doctrine.php';


$profiler = new Doctrine_Connection_Profiler();

$conn = Doctrine_Manager::connection();
$conn->setListener($profiler);

$t = new lime_test(0, new lime_output_color());

class MyDestructableClass {
   function __construct() {
       print "\nIn constructor\n";
       $this->name = "MyDestructableClass";
   }

   function __destruct() {
       print "\nDestroying " . $this->name . "\n";
   }
}

$obj = new MyDestructableClass();
$news = new News();

$news->setUrl('http://test');
$news->setHash('http://test');
$news->setTitle('http://test');
$news->setSummarize('http://test');
$news->setAccountId(1);
$news->setCategoryId(1);
$news->setThumbnail('http://test');
$news->setCreatedAt(date('Y-m-d H:i:s',time()));
$news->setUpdatedAt(date('Y-m-d H:i:s',time()));
$news->save();
exit();

12
与您的问题无关,但 $this->name 是不必要的——相反,应使用特殊常量 __CLASS__ - Frank Farmer
1
获取类名($this); - Beachhouse
@Beachhouse get_class() 是一个棘手的函数,如果你不了解它的特性,它可能会表现得非常出乎意料。例如,如果你在一个非对象(null)上调用 get_class(),它将返回当前类的名称,非常令人困惑。我甚至建议完全抵制这个函数。 - Daniel W.
7个回答

81

__destruct方法将不会被调用:

  • 如果另一个析构函数中调用了exit
  • 取决于PHP版本: 如果在使用register_shutdown_function注册的关机函数中调用了exit
  • 如果代码中存在致命错误
  • 如果另一个析构函数抛出异常
  • 如果尝试在析构函数中处理异常 (PHP >= 5.3.0)

我猜现在想到的就这些了。

& Pascal MARTIN所说的那样。 这是调试的第一步。


1
我猜在所有这些情况下,PHP都会进入“关闭序列”。更多信息请参见https://dev59.com/R3VC5IYBdhLWcg3w51hv#8293937。 - Mikko Rantalainen
1
抱歉 - 我知道这很老了,但是:如果进程被杀死怎么办? - dmgig
6
如果进程被杀死,进程就会死亡。它不会被问及是否喜欢死亡,也没有时间在死亡前做任何事情,它只会立即死亡。因此,__destruct() 也不会在这种情况下执行。 - Shi

20

__destruct 方法也不会在脚本运行于 CLI 并接收到 SIGINT (Ctrl+C) 时被调用。


2
只是澄清一下,<kbd>Ctrl</kbd>+<kbd>C</kbd> 通常会发送 SIGINT 信号(而不是 SIGTERM)。但确实在这种情况下也不会调用 __destruct - Ondrej Machulda
@OndrejMachulda 谢谢,你说得对。我修改了答案。 - nickel715

14

没有在屏幕上输出并不意味着析构函数未被调用:输出可以使用输出缓冲区(也许Lime这样做是为了能够处理输出?)捕获,然后在脚本结束时不回显,例如。

为了测试目的,您可以尝试在__destruct方法中写入文件,而不仅仅是回显一些文本。
(只需确保您的应用程序/PHP具有写入目标文件所需的权限即可)

(我已经遇到过即使析构函数被调用,也看不到产生的输出的情况)


据我所知,甚至在调用__destruct时数据库连接也会关闭。 - TheHippo
我使用了 file_put_contents 进行测试以确保它没有被调用。但是出现了 PHP 致命错误 - user198729
致命错误提示了什么?它是否指示您的脚本存在问题,或者某些东西没有按预期工作? - Pascal MARTIN
1
根据register_shutdown_function的描述,当前目录可能会在回调函数中发生不可预知的改变。请注意。 - polkovnikov.ph

12

正如PHP文档所述:

即使使用exit()停止脚本执行,析构函数也将被调用。在析构函数中调用exit()将阻止剩余的关闭例程执行。


5

我知道我来晚了,但是对于那些也希望在按下 CTRL+C 和/或发生致命错误时执行 __destruct 的人,可以尝试以下代码(下面是一个测试案例):

Index.php

<?php

// Setup CTRL+C and System kill message handler
// The only signal that cannot be caught is the SIGKILL (very hard kill)
declare(ticks = 1); // Required else it won't work.
pcntl_signal(SIGTERM, 'close'); // System kill (Unhappy Termination)
pcntl_signal(SIGINT, 'close'); // CTRL+C (Happy Termination)

// Shutdown functions will be executed even on fatal errors
register_shutdown_function('close');

function close($signal = null) // only pcntl_signal fills $signal so null is required
{
    // Check if there was an fatal error (else code below isn't needed)
    $err = error_get_last();
    if(is_array($err))
    {
        foreach(array_keys($GLOBALS) as $key)
        {
            if(in_array($key, ['_GET', '_POST', '_COOKIE', '_FILES', '_SERVER', '_REQUEST', '_ENV', 'GLOBALS']))
                continue;

            // This will automatically call __destruct
            unset($GLOBALS[$key]);
        }
    }
}

// Example
class blah
{
    private $id = '';

    public function __construct()
    {
        $this->id = uniqid();
        // note this piece of code, doesn't work on windows!
        exec('mkdir /tmp/test_'.$this->id);
    }

    public function __destruct()
    {
        // note this piece of code, doesn't work on windows!
        exec('rm /tmp/test_'.$this->id.' -R');
    }
}

// Test
$a = new blah();
$b = new blah();
$c = new blah();
$d = new blah();
$e = new blah();
$f = new blah();
$g = new blah();
$h = new blah();
$i = new blah();
$j = new blah();
$k = new blah();
$l = new blah();
$m = new blah();
$n = new blah();
$o = new blah();
$p = new blah();
$q = new blah();
$r = new blah();
$s = new blah();
$t = new blah();
$u = new blah();
$v = new blah();
$w = new blah();
$x = new blah();
$y = new blah();
$z = new blah();

// The script that causes an fatal error
require_once(__DIR__.'/test.php');

Test.php

<?php

// this will create a parse (E_PARSE) error.
asdsaddsaadsasd

注意:在析构函数或关机函数中调用exit或抛出异常将立即导致脚本终止。

3
如果你将一个类的实例的引用传递给另一个类,并在该类外部调用 exit,那么该类的 __destruct 方法将不会被调用。这对我来说看起来像是一个 bug。
$a = new classa();
$b = new classb($a);  // where classb constructor is __construct(&a) {}
exit;                 

两个析构函数都不会被调用,析构顺序也没有保证。我尝试了很多方法来在析构函数中输出消息,除非我明确地指定,否则它永远不会打印:

unset($b);     // properly calls classb::__destruct
unset($a);     // properly calls classa::__destruct
exit;

由于我无法获得任何结果,所以我无法确定这是否是析构函数的竞态条件还是预期的结果; 不管怎样,unset()总是正确地调用析构函数。我知道这很痛苦,但比留下这个bug更好。当调用exit并按正确顺序调用析构函数时,它们需要正确处理类的引用计数和依赖关系顺序。


-4

虽然不熟悉Doctrine,但需要检查一点:在__construct()/__destruct()中检查可能的异常,它们可能会产生致命错误。


如果你遇到了致命错误,任何执行的操作都将立即停止。这时要找到发生错误的位置会很困难。 - Alexey Shein
@SheinAlexey难以找到发生错误的地方?这是错误的。PHP解析器将准确显示该错误的行号、编号和原因。 - Yang

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接