In which order are objects destructed in PHP?
Asked Answered
M

3

25

What is the exact order of object deconstruction?

From testing, I have an idea: FIFO for the current scope.

class test1
{
    public function __destruct()
    {
        echo "test1\n";
    }
}

class test2
{
    public function __destruct()
    {
        echo "test2\n";
    }
}

$a = new test1();
$b = new test2();

Which produces the same results time and time again:

test1
test2

The PHP manual is vague (emphasis mine to highlight uncertainty): "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."

What is the exact order of deconstruction? Can anyone describe in details the implementation of destruction order that PHP uses? And, if this order is not consistent between all the PHP versions, can anyone pinpoint which PHP versions change in this order?

Microscopic answered 31/12, 2012 at 1:44 Comment(21)
If the documentation says it can be any order, you'd be very ill-advised to assume any particular order, even if the order appears to be stable in all your tests and in all versions you care about. Why do you need this anyway? If it matters, you should just be explicit about it instead of relying on finalization. This goes for any language btw.Hillegass
Why does the exact order matter? If the manual clearly states that it's "any order", you should never ever rely on it.Wellmannered
"Any order" doesn't imply that it's random, just that the order is not guaranteed. It can still have a deterministic order based on the implementation details.Doodlebug
It's not random in practice, but you should write your code as if it were. A good rule of thumb for understanding the word "arbitrary".Wellmannered
maybe this will help a little: Garbage CollectorMamoun
I appreciate the comments about relying on order being bad practice, but no definitive answers? "Because it's bad" or "because it says so" is hardly ever a correct answer. Why I need this information doesn't matter. I'm just looking for the implementation. A line-by-line explanation of the PHP sourcecode involved would be nice too, but any explanation at all would suffice.Microscopic
@Microscopic If you need this information you either have a cruel boss, or you did something terribly wrong.Electrocute
@Microscopic i think you are overthinking. The reason they are telling you "why do you need it" is very valid. If i was to ask how can i manually release every variable from memory using php, sure you can but it will be irrelevant. The same thing with your questionGeneral
Are you looking for a diagram of the shutdown process of a request? I seriously doubt you're going to get a line-by-line source code explanation (and is that really desirable?). Give us one or a few scenarios about hypothetical concerns. It is important why you're asking, because it seems a bit nonsensical in practice.Holloweyed
@Ibu: "don't worry about it" is the impression I'm getting from comments. I'd appreciate more than just that. A full explanation would be better. An answer describing the process of selecting which destructors to call when would be best.Microscopic
@Microscopic I have never needed such thing in php. Regarding sources, those are publicly available, so i guess you can take a look, but i doubt is gonna be easy to follow. Basicly, unset is the first one to initiate a __destruct call. Then refcount is the next one. After that, i pretty much doubt you care. If you need to control the destruct sequence, you can do so by manually unsetting whatever objects you need unset. I am pretty sure that you are very less likely to write a application taking the order of __destruct calls into account.Mamoun
Is this request-response diagram sort've what you're thinking? (That one's not very detailed, either.) I feel like I've seen a class or object diagram at some point in the past. I don't know if I'll be able to find it again, though. APC maybe could do it?Holloweyed
This isn't up for a debate. PHP calls __destruct automatically for a reason and likely in a predictable order. What is that order? What is the algorithm to determine this order?Microscopic
Are you talking to me? I'm the only one trying to figure out what you're looking for here. Maybe something like this? Or should I interpret that response as a cue to move on?Holloweyed
@Jared Farrish: no, not talking to you. I would hope a question like "what order is __destruct called" is a simple x->y->z according to how it's exactly designed to work. Too many comments, not enough answers to this post.Microscopic
Are you coming from C or some other low level language where you had or knew absolutely what and when and how something happened, probably because you had to? PHP isn't like that. It's higher up, so these low-level tasks are completely managed internally in such a manner that we don't know, just like GC. It happens when it happens and the engineering behind it is sometimes... unusual. Honestly, you'd probably have to find a PHP source engineer or (as I'm doing) try to find a tool that maps process flows and visually depicts them. It's really hard to say.Holloweyed
What everybody is trying to say is that even if you find the answer to this, there's no practical use for it in PHP programming. None, at all. Surely, random isn't the exact term to describe it, but when it comes to relying on that order - that's how you should think of it. You should use register_shutdown_function() if you need to rely on a particular order.Overweary
@Narf: underneath OOP's hood is pure procedural no matter how deep you go. It is important to know exactly in which order magic methods are called, and within those methods, the order of all statements called or executed. Again though, the order of calling __destruct isn't up for a debate. It's called. I'd like to know when.Microscopic
Hey, IRCMaxwell's an SO user whose on the chat rooms a lot. That's basically an overview of the source code and how to understand it. Honestly, you probably really should have posted this as a C question, not PHP.Holloweyed
@Jared Ferrish: this is a PHP application-specific question, not c or c++. While I'd hope that this information is more widely known about PHP, it's just not the case and I realize I should just engage PHP devs directly like you suggested. Of course this is a rare case scenario, but it should still be documented properly instead of just more or less saying "it's random" in the docs.Microscopic
I don't get it. It's really hard to understand where you're coming from on this... whatever it is. It's PHP, it's like asking a race car driver how the ADA code sequences the piston order under high torque. What? Talk to the engineers. We utilize PHP as it's given to us. If you don't care for that, I'm guessing you probably should find another language.Holloweyed
B
40

First of all, a bit on general object destruction order is covered here: https://mcmap.net/q/538659/-what-determines-when-a-class-object-is-destroyed-in-php

In this answer I will only concern myself with what happens when objects are still alive during the request shutdown, i.e. if they were not previously destroyed through the refcounting mechanism or the circular garbage collector.

The PHP request shutdown is handled in the php_request_shutdown function. The first step during the shutdown is calling the registered shutdown functions and subsequently freeing them. This can obviously also result in objects being destructed if one of the shutdown functions was holding the last reference to some object (or if the shutdown function itself was an object, e.g. a closure).

After the shutdown functions have run the next step is the one interesting to you: PHP will run zend_call_destructors, which then invokes shutdown_destructors. This function will (try to) call all destructors in three steps:

  1. First PHP will try to destroy the objects in the global symbol table. The way in which this happens is rather interesting, so I reproduced the code below:

    int symbols;
    do {
        symbols = zend_hash_num_elements(&EG(symbol_table));
        zend_hash_reverse_apply(&EG(symbol_table), (apply_func_t) zval_call_destructor TSRMLS_CC);
    } while (symbols != zend_hash_num_elements(&EG(symbol_table)));
    

    The zend_hash_reverse_apply function will walk the symbol table backwards, i.e. start with the variable that was created last and going towards the variable that was created first. While walking it will destroy all objects with refcount 1. This iteration is performed until no further objects are destroyed with it.

    So what this basically does is a) remove all unused objects in the global symbol table b) if there are new unused objects, remove them too c) and so on. This way of destruction is used so objects can depend on other objects in the destructor. This usually works fine, unless the objects in the global scope have complicated (e.g. circular) interrelations.

    The destruction of the global symbol table differs significantly from the destruction of all other symbol tables. Normally symbol tables are destructed by walking them forward and just dropping the refcount on all objects. For the global symbol table on the other hand PHP uses a smarter algorithm that tries to respect object dependencies.

  2. The second step is calling all remaining destructors:

    zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC);
    

    This will walk all objects (in order of creation) and call their destructor. Note that this only calls the "dtor" handler, but not the "free" handler. This distinction is internally important and basically means that PHP will only call __destruct, but will not actually destroy the object (or even change its refcount). So if other objects reference the dtored object, it will still be available (even though the destructor was already called). They will be using some kind of "half-destroyed" object, in a sense (see example below).

  3. In case the execution is stopped while calling the destructors (e.g. due to a die) the remaining destructors are not called. Instead PHP will mark the objects are already destructed:

    zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC);
    

    The important lesson here is that in PHP a destructor is not necessarily called. The cases when this happens are rather rare, but it can happen. Furthermore this means that after this point no more destructors will be called, so the remainder of the (rather complicated) shutdown procedure does not matter anymore. At some point during the shutdown all the objects will be freed, but as the destructors have already been called this is not noticeable for userland.

I should point out that this is the shutdown order as it currently is. This has changed in the past and may change in the future. It's not something you should rely on.

Example for using an already destructed object

Here is an example showing that it is sometimes possible to use an object that already had its destructor called:

<?php

class A {
    public $state = 'not destructed';
    
    public function __destruct() { $this->state = 'destructed'; }
}

class B {
    protected $a;
    
    public function __construct(A $a) { $this->a = $a; }
    
    public function __destruct() { var_dump($this->a->state); }
}
    
$a = new A;
$b = new B($a);

// prevent early destruction by binding to an error handler (one of the last things that is freed)
set_error_handler(function() use($b) {});

The above script will output destructed.

Barbarous answered 31/12, 2012 at 12:43 Comment(3)
Thanks! While I understand it's bad form to rely on this behavior and that it's not something most PHP users will experience, I still wanted to know how it works under the hood, and if it actually can or cannot be predicted. This info here will help me to avoid mistakes and more easily isolate bugs/bad code.Microscopic
The example of using a 'destructed' object, helped me figure out what was going on in some legacy code. An 'Every thing including the kitchen sink' object was rendering on destruction, but after it's helper objects had been destructed. Turns out the error handler keeps it alive until the very end!Niveous
Yet in case of circular reference (and any objects held/referenced by those circular referencing objects), the order of destruction is not guaranteed in zend_objects_store_call_destructors, because "slot" in objects_store could be reused, the order is simplying not the order of object creation.Pocketbook
E
8

What is the exact order of deconstruction? Can anyone describe in detail the implementation of destruction order that PHP uses? And, if this order is not consistent between any and all PHP versions, can anyone pinpoint which PHP versions this order changes in?

I can answer three of these for you, in a somewhat roundabout way.

The exact order of destruction is not always clear, but is always consistent given a single script and PHP version. That is, the same script running with the same parameters that creates objects in the same order will basically always get the same destruction order as long as it runs on the same PHP version.

The shutdown process -- the thing that triggers object destruction when script execution has stopped -- has changed in the recent past, at least twice in a way that impacted the destruction order indirectly. One of these two introduced bugs in some old code I had to maintain.

The big one was back in 5.1. Prior to 5.1, the user's session was written to disk at the very start of the shutdown sequence, before object destruction. This meant that session handlers could access anything that was left over object-wise, like, say, custom database access objects. In 5.1, sessions were written after one sweep of object destruction. In order to retain the previous behavior, you had to manually register a shutdown function (which are run in order of definition at the start of shutdown before destruction) in order to successfully write session data if the write routines needed a (global) object.

It is not clear if the 5.1 change was intended or was a bug. I've seen both claimed.

The next change was in 5.3, with the introduction of the new garbage collection system. While the order of operations at shutdown remained the same, the precise order of destruction could now change based on ref counting and other delightful horrors.

NikiC's answer has details on the current (at time of writing) internal implementation of the shutdown process.

Once again, this is not guaranteed anywhere, and the documentation very expressly tells you to never assume a destruction order.

Emyle answered 31/12, 2012 at 8:1 Comment(1)
wow. Thanks. If I could accept both NikiC's and your answers, I would. I had a feeling implementation would not be the same across all versions and wanted to be sure. Session handlers would definitely be a gotcha in this area, and I'm glad the manual there (php.net/manual/en/function.session-set-save-handler.php) gives warnings and workarounds.Microscopic
B
0

For anyone interested - as at PHP 8.0:

class A {
  
  function __destruct() {
    print get_class();
  }
}

class B {
  private $child;

  function __construct() {
    $this->child = new A();
  }
  
  function __destruct() {
    print get_class();
  }
}

class C {
  private $child;

  function __construct() {
    $this->child = new B();
  }
  
  function __destruct() {
    print get_class();
  }
}


new C;

results in output of

CBA

ie. the containing object destructor fires before the contained object destructor.

To reverse the order if desired ie. to ABC, change the destructor in all but A (innermost class) to be:

function __destruct() {
  unset($this->child);
  print get_class();
}
Bigoted answered 18/12, 2021 at 13:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.