After much tracking down I finally figured out what's going wrong in my code, so this question isn't "how do I fix it", but rather "why does this happen?".
Consider the following code:
class Foo {
private $id;
public $handle;
public function __construct($id) {
$this->id = $id;
$this->handle = fopen('php://memory', 'r+');
echo $this->id . ' - construct' . PHP_EOL;
}
public function __destruct() {
echo $this->id . ' - destruct' . PHP_EOL;
fclose($this->handle);
}
public function bar() {
echo $this->id . ' - bar - ' . get_resource_type($this->handle) . PHP_EOL;
return $this;
}
public static function create($id) {
return new Foo($id);
}
}
Seems simple enough - when created it will open up a memory stream and set the property $handle
and $id
. When destructing it will use fclose
to close this stream.
Usage:
$foo = Foo::create(1); // works
var_dump( $foo->bar()->handle ); // works
var_dump( Foo::create(2)->bar()->handle ); // doesn't work
What seems to be the issue here is that I'm expecting both calls to return exactly the same but for some reason the Foo::create(2)
call where I don't save the instance to a variable calls the garbage collector somewhere between the return $this
part of the bar()
method and me actually using the property $handle
.
In case you're wondering, this is the output:
1 - construct // echo $this->id . ' - construct' . PHP_EOL;
1 - bar - stream // echo $this->id . ' - bar - ' ...
resource(5) of type (stream) // var_dump
2 - construct // echo $this->id . ' - construct' . PHP_EOL;
2 - bar - stream // echo $this->id . ' - bar - ' ...
2 - destruct // echo $this->id . ' - destruct' . PHP_EOL;
resource(6) of type (Unknown) // var_dump
1 - destruct // echo $this->id . ' - destruct' . PHP_EOL;
From what I can see this is what happens:
var_dump( Foo::create(2)->bar()->handle );
// run GC before continuing.. ^^ .. but I'm not done with it :(
But why? Why does PHP think I'm done with the variable/class instance and hence feels the need to destruct it?
Demos:
eval.in demo
3v4l demo (only HHVM can figure it out - all other PHP versions can't)
This all boils down to refcounts and how PHP treats resources differently.
When a class instance is destroyed, all non-database link resources are closed (see above link on resources). All non-resources referenced elsewhere will still be valid.
In your first example you assign $temp = Foo::create(1)
which increases the refcount to an instance of Foo
, preventing it from being destroyed which keeps the resource open.
In your second example, var_dump( Foo::create(2)->bar()->handle );
, here's how things play out:
Foo::create(2)
is called, creating an instance of Foo
.bar()
on the new instance, returning $this
which increases the refcount by one.bar()
's scope and the next action isn't a method call or an assignment, refcount goes down by one.Unknown
.As additional proof, this works just fine:
$temp = Foo::create(3)->bar();
// $temp keep's Foo::create(3)'s refcount above zero
var_dump( $temp->handle );
As does this:
$temp = Foo::create(4)->bar()->bar()->bar();
// Same as previous example
var_dump( $temp->handle );
And this:
// Assuming you made "id" public.
// Foo is destroyed, but "id" isn't a resource. It will be garbage collected later.
var_dump( Foo::create(5)->id );
This doesn't work:
$temp = Foo::create(6)->handle;
// Nothing has a reference to Foo, it gets destroyed, all resources closed.
var_dump($temp);
Neither does this:
$temp = Foo::create(7);
$handle = $temp->handle;
unset($temp);
// $handle is now a reference to a closed resource because Foo was destroyed
var_dump($handle);
When Foo
is destroyed, all open resources (except database links) are closed. References other properties from Foo
are still valid.
Demos: https://eval.in/271514
It seems that it's all about variable scoping.
In short if you assign
Foo::create()
to a global variable you can access thehandle
in the global scope and the destructor won't be called until the end of the script.Whereas if you don't actually assign it to a global variable the last method call in the local scope will trigger the destructor; the handle is closed at
Foo::create(1)->bar()
so->method
is now closed when you're attempting to access it.
Further investigation reveals that premise is flawed - there's definitely something hinky going on here! It only seems to affect resources.
case 1
$foo = Foo::create(1);
var_dump( $foo->bar()->handle );
Results in:
resource(3) of type (stream)
In this case we have assigned the global variable $foo
to be a new instance of Foo
created with Foo::create(1)
. We're now accessing that global variable with bar()
to return itself and then the public handle
.
case 2
$bar = Foo::create(2)->bar();
var_dump( $bar->handle );
Results in:
resource(4) of type (stream)
Again, it's still OK because Foo::create(2)
has created a new instance of Foo
and bar()
has simply returned it (it still had access to it within the local scope). This has been assigned to the global variable $bar
and it's from that, that handle
is being retrieved.
case 3
var_dump( Foo::create(3)->bar()->handle );
Results in:
resource(5) of type (Unknown)
This is because when Foo::create()
returns a new instance of Foo
, that's used by bar()
... however when bar()
closes there's no longer any local use of that instance and the __destruct()
method is called which closes the handle. It's the same result you'd get if you simply wrote:
$h = fopen('php://memory', 'r+');
fclose($h);
var_dump($h);
You get exactly the same result if you try:
var_dump( Foo::create(3)->handle );
Foo::create(3)
will call the destructor because there are no more local calls to that instance.
EDIT
Further tinkering has muddied the waters further...
I've added this method:
public function handle() {
return $this->handle;
}
Now if my premise was right, doing:
var_dump( Foo::create(3)->handle() );
should have resulted in:
resource(3) of type (stream)
... but it doesn't, again you get a resource type of Unknown - it seems the destructor is called at return $this
before the public class member is accessed! Yet it's absolutely fine to call a method on it:
public function handle() {
return $this->bar();
}
That will quite happily give you your object back:
object(Foo)#1 (2) {
["id":"Foo":private]=>
int(3)
["handle"]=>
resource(3) of type (stream)
}
It seems there's no way to access resource class members, in this fashion, before the destructor is called?!
As Alex Howansky points out, it's fine with scalars:
public function __destruct() {
$this->id = 2000;
fclose($this->handle);
}
public function handle() {
return $this->id;
}
Now:
var_dump( Foo::create(3)->handle() );
Results in:
int(3)
... the original $id was returned before the destructor was called.
This definitely smells like a bug to me.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With