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:
-
He is creating a specific method (unless adding a reference anywhere in the class will force the entire class to change its behaviour)
-
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
It looks backwards, but it does make sense. What’s happening is
echo
, so that "method" (it’s really a language construct) has been passed a copy of the value to output. Remember, PHP passes by valueecho
executes with the value it’s already been givenIf you assign your class to a variable, it works the other way around
produces this result
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.
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:
Compiles to this:
Without going into too much detail:
$0
is the internal variable holding the object: it’s created by theNEW
opcode, and then used by theFETCH_OBJ_R
opcode to look up the ‘name’ property~2
is the internal variable holding the result of theFETCH_OBJ_R
opcodeECHO
opcode only needs~2
, not$0
, so between these two operations,$0
will be discarded, and the destructor triggeredIn other words, it’s roughly equivalent to this code:
Another way to see this is by replacing the property access with a method call:
From the output, it’s clear that although we don’t see the name until after the object is destroyed, it was retrieved first: