php destructor called too soon with fluent interface

拈花ヽ惹草 提交于 2019-12-08 01:34:26

问题


I found a really weird thing about php destructor:

basically I have a database management class which loads an adapter using a factory to define which adapter should be loaded (mysql, mysqli, etc.)

I'll write down only the part of the code insteresting, as the class itself is way longer but code isn't involved in the current trouble

The problem occurs ONLY with mysql (mysqli & pdo works just fine) but for compatibility purposes, getting rid of mysql it's out of question.

class manager
{
    private static $_instance;

    public static function getInstance()
    {
        return isset(self::$_instance) ? self::$_instance : self::$_instance = new self;
    }

    public function getStandaloneAdapter()
    {
        return new mysql_adapter(array('host'=>'127.0.0.1', 'username' => 'root', 'password' => '', 'dbname' => 'etab_21'));
    }
}

abstract class abstract_adapter
{
    protected $_params;
    protected $_connection;

    public function __construct($params)
    {
        $this->_params = (object)$params;
    }

    public function __destruct()
    {
        echo 'destructor<br/>';
        var_dump(debug_backtrace(false));
        $this->closeConnection();
    }

    abstract public function closeConnection();
}

class mysql_adapter extends abstract_adapter
{
    public function getConnection()
    {
        $this->_connect();

        if ($this->_connection) {
            // switch database
            $this->_useDB($this->_params->dbname);
        }

        return $this->_connection;
    }

    protected function _connect()
    {
        if ($this->_connection) {
            return;
        }

        // connect
        $this->_connection = mysql_connect(
            $this->_params->host,
            $this->_params->username,
            $this->_params->password,
            true
        );

        if (false === $this->_connection || mysql_errno($this->_connection)) {
            $this->closeConnection();
            throw new Mv_Core_Db_Exception(null, Mv_Core_Db_Exception::CONNECT, mysql_error());
        }

        if ($this->_params->dbname) {
            $this->_useDB($this->_params->dbname);
        }
    }

    private function _useDB($dbname)
    {
        return mysql_select_db($dbname, $this->_connection);
    }

    public function isConnected()
    {
        $isConnected = false;
        if (is_resource($this->_connection)) {
            $isConnected = mysql_ping($this->_connection);
        }
        return $isConnected;
    }

    public function closeConnection()
    {
        if ($this->isConnected()) {
            mysql_close($this->_connection);
        }
        $this->_connection = null;
    }
}

so here's the test i'm running:

$sadb1 = manager::getInstance()->getStandaloneAdapter()->getConnection();
var_dump($sadb1);

and the output i'm getting:

destructor
array
  0 => 
    array
      'file' => string '**\index.php' (length=48)
      'line' => int 119
      'function' => string '__destruct' (length=10)
      'class' => string 'abstract_adapter' (length=16)
      'type' => string '->' (length=2)
      'args' => 
        array
          empty
  1 => 
    array
      'file' => string '**\index.php' (length=48)
      'line' => int 119
      'function' => string 'unknown' (length=7)
resource(26, Unknown)

if i change my test to this:

$sadb1 = manager::getInstance()->getStandaloneAdapter();
var_dump($sadb1->getConnection());

the output is good:

resource(26, mysql link)
destructor
array
  0 => 
    array
      'function' => string '__destruct' (length=10)
      'class' => string 'abstract_adapter' (length=16)
      'type' => string '->' (length=2)
      'args' => 
        array
          empty

wtf?!


回答1:


The early destructor run in the first test is an outcome of automatic garbage collection. To understand this, let's look at the second (easier) test:

1. $db = Mv_Core_Db_Manager::getInstance();
2. $sadb = $db->getStandaloneAdapter('bdm_bcb');
3. var_dump($sadb->getConnection());

Steps:

  1. the db manager is being assigned to the variable $db,
  2. the standalone adapter (MySQL adapter in this case due to hack in the factory you have introduced for debugging) is being assigned to $sadb,
  3. var_dump() debugs the return value of the getConnection() method of the $sadb standalone adapter, which is the resource(29, mysql link) line in your second output,
  4. clean up time; PHP garbage collector runs the destructor for $sadb (visible in your output thanks to debug) and after that $db (not visible in your output).

Here garbage collection is hapenning at the end.

If you consider the first test you have described, despite deceivingly similar looking source code, it has different steps:

1. $db = Mv_Core_Db_Manager::getInstance();
2. $sadb = $db->getStandaloneAdapter('bdm_bcb')->getConnection();
3. var_dump($sadb);

Steps:

  1. the same in the test case above,
  2. the return value of the getConnection() getter of the MySQL standalone adapter object is assigned to $sadb,
  3. because the MySQL standalone adapter itself has not been assigned to any variable, PHP garbage collector decides that it's not used anymore so it cleans up the object and runs its destructor (the destructor debug is visible in your output in first),
  4. var_dump() debugs the value returned by the of the getConnection() getter of the MySQL standalone adapter, which is basically a handle to a resource which has already been collected by the garbage collector.

Here garbage collection happens before var_dump().

To summarize, the first test you have provided forces the garbage collector to jump in between lines 2 and 3 of your code. On the other hand, the second test forces the garbage collection at the very end.

The outcome is that your resource handle is pointing to memory that has been already cleaned up by the GC.




回答2:


Based on the code presented...

PHP __destruct()

The __destruct() method, according to the documentation:

The destructor method will be called as soon as there are no other references to a particular object, or in any order during the shutdown sequence.

The reason that you would experience different results with:

$sadb1 = manager::getInstance()->getStandaloneAdapter()->getConnection();
var_dump($sadb1);

Giving you incorrect results (from what you are expecting) is that there are no more references to that mysql_adapter instance in your code, therefor the __destruct() method is called, doing so closes the reference to the resource that held the mysql link - as PHP5 is smart and passes most things auto-magically by reference (good thing - most of the time ^^), so when you var_dump($sadb);, the __destruct method was called on the previous line, so the var_dump gives you a reference, but now to nothing.

The reason this code gives you what you expect:

$sadb1 = manager::getInstance()->getStandaloneAdapter();
var_dump($sadb1->getConnection());

is that you dump the resource, and then the __destruct method is called, giving you the debug_trace, following the dump.

I hope that helps clear things up for you with regards to the destructor.

Out of curiosity, why is b 'sad'? ($sadb) ^^



来源:https://stackoverflow.com/questions/16163812/php-destructor-called-too-soon-with-fluent-interface

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!