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 fruit\n";
}
}
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?
CodePudding user response:
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 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
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 fruit\n";
}
}
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
CodePudding user response:
It looks backwards, but it does make sense. What's happening is
- 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 - Upon doing so, there are no remaining references to the class, so garbage collection kicks in and destroys the class, dutifully calling the destructor
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 force the class to be passed by reference (this generates an E_NOTICE
, so it's not proper PHP for the sake of example). 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"));