skip to Main Content

TL;DR >

echo (new ClassName())->propertyName;

Will call the __destruct(), run its code and AFTER THAT successfully retrieve its "propertyName" property (which will be echoed normally). How can it be retrieving a property from a "destroyed" (or unset) object?

< TL;DR


I have tried to find this online (sorry if it’s been already answered). The explanation on https://www.php.net/manual/en/language.oop5.decon.php didn’t quite explain this to me.

I’m using https://onlinephp.io/ to test this. Just for the sake of it, I’m using both PHP versions 7.0.33 and 8.1.8 (but they seem to behave the same way about it).

The code is as follows:

class Fruit {
  public $name;
  
  public function __construct($n = "Fruit") {
    $this->name = $n;
  }
  
  public function __destruct() {
    echo "nBye bye fruitn";
  }
}

Given this Fruit class, I will create 2 instances of it – Apple and Banana – and retrieve and echo its "name" property.

Apple will be assigned to a variable.

$apple = new Fruit("Apple");
unset($apple);
echo $apple->name . "n";

This will echo the "Bye bye fruit" as expected (from the __destruct()), and then give me a warning, since I am trying to access its property after the object has been unset.

Bye bye fruit

Warning: Undefined variable $apple in /home/user/scripts/code.php on line X

Warning: Attempt to read property "name" on null in /home/user/scripts/code.php on line X

That’s expected, since there’s no $apple anymore.

HOWEVER

echo (new Fruit("Banana"))->name;

Will output:

Bye bye fruit
Banana

I’m not assigning it to any variable, so I’m assuming it’s being destroyed (unset?) by the garbage collector.

Nonetheless, the __destruct() is being called, as we can see the output "Bye bye fruit".
If it had output (echoed) the property ("Banana") first, I’d understand, but this seems counterintuitive to me.

How is it possible that the property is being accessed? Where does it "live"?

P.S.

I did find this 14 years old comment on the php.net reference page (link above)

[...]
    public static function destroyAfter(&$obj)
    {
        self::getInstance()->objs[] =& $obj;
        /*
            Hopefully by forcing a reference to another object to exist
            inside this class, the referenced object will need to be destroyed
            before garbage collection can occur on this object.  This will force
            this object's destruct method to be fired AFTER the destructors of
            all the objects referenced here.
        */
    }
[...]

But, even if it give any clue, it doesn’t seem to be THE explanation, because:

  1. He is creating a specific method (unless adding a reference anywhere in the class will force the entire class to change its behaviour)

  2. Even if properties inside a class have their own "reference", how can the application still find it "through the object", given that it was already destroyed?

2

Answers


  1. It looks backwards, but it does make sense. What’s happening is

    1. PHP passes the value to echo, so that "method" (it’s really a language construct) has been passed a copy of the value to output. Remember, PHP passes by value
    2. Upon doing so, there are no remaining references to the class, so garbage collection kicks in and destroys the class, dutifully calling the destructor
    3. echo executes with the value it’s already been given

    If you assign your class to a variable, it works the other way around

    $fruit = new Fruit("Banana");
    echo $fruit->name;
    

    produces this result

    Banana
    Bye bye fruit

    The difference is that the script has to end first before the object is destroyed. It also happens this way if you use the class in a function (unlike variables, classes are always passed by reference). This does the same thing because the function is getting the created instance of the class. Thus the function has to end before the destructor can be called.

    function showFruit(Fruit $fruit) {
        echo $fruit->name;
    }
    showFruit(new Fruit("Banana"));
    
    Login or Signup to reply.
  2. The destructor is actually being called after retrieving the property, but before echoing it.

    If we get a representation of how the code is compiled, we can see that this line:

    echo (new Fruit("Banana"))->name;
    

    Compiles to this:

    line      #* E I O op                           fetch          ext  return  operands
    -------------------------------------------------------------------------------------
       15     0  E >   NEW                                              $0      'Fruit'
              1        SEND_VAL_EX                                              'Banana'
              2        DO_FCALL                                      0          
              3        FETCH_OBJ_R                                      ~2      $0, 'name'
              4        ECHO                                                     ~2
       16     5      > RETURN                                                   1
    

    Without going into too much detail:

    • $0 is the internal variable holding the object: it’s created by the NEW opcode, and then used by the FETCH_OBJ_R opcode to look up the ‘name’ property
    • ~2 is the internal variable holding the result of the FETCH_OBJ_R opcode
    • the ECHO opcode only needs ~2, not $0, so between these two operations, $0 will be discarded, and the destructor triggered

    In other words, it’s roughly equivalent to this code:

    $_0 = (new Fruit("Banana"));
    $_2 = $_0->name;
    unset($_0);
    echo $_2;
    

    Another way to see this is by replacing the property access with a method call:

    class Fruit {
      private $name;
        
      public function __construct($n = "Fruit") {
        $this->name = $n;
      }
      
      public function getName() {
          echo "nGetting name...n";
          return $this->name;
      }
      
      
      public function __destruct() {
        echo "nBye bye fruitn";
      }
    }
    
    echo (new Fruit("Banana"))->getName();
    

    From the output, it’s clear that although we don’t see the name until after the object is destroyed, it was retrieved first:

    Getting name...
    
    Bye bye fruit
    Banana
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search