4

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)

18
  • Maybe after the bar call the GC kills the object because it isn't assigned to a variable? I mean, it may have operations after the bar call but, as it isn't assigned to anything, it doesn't care and kills it off anyway. Commented Jan 28, 2015 at 15:15
  • @JamesHunt But it's still defined in the class ($this->handle) and it's returning the class instance (where $instance->handle is defined)? Commented Jan 28, 2015 at 15:16
  • Handle may be defined within the class, but the object itself still needs to be assigned to a variable for var_dump. var_dump is called with your argument, your argument uses the static function Foo::create(2) to create an instance of the class Foo, where you then use it's bar function. After the bar function is finished, the object itself isn't stored in a variable, it's just floating around as a possible argument, so maybe the GC is wiping it out before var_dump can get a hold of the handle variable? Commented Jan 28, 2015 at 15:23
  • 1
    After some playing around, I've come to the conclusion that this is a bit weird... if, in your create method you assign new Foo() to a variable and then return that variable it works fine too. create($id) { $foo = new Foo($id); return $foo; } then it's OK - even though the GC will destroy $foo at the close of the method (when $foo is returned). This may have something to do with the fact that constructors return null maybe - though I'm clutching at straws a bit here... Commented Jan 28, 2015 at 16:08
  • 1
    It also seems to work if you wrap some parentheses around it : create($id) { return (new Foo($id)); } - now I'm beginning to think this is to do with class member access on instantiation added in PHP 5.4 : docs.php.net/manual/en/migration54.new-features.php Commented Jan 28, 2015 at 16:12

2 Answers 2

9
+300

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:

  1. Foo::create(2) is called, creating an instance of Foo.
  2. You call method bar() on the new instance, returning $this which increases the refcount by one.
  3. You leave bar()'s scope and the next action isn't a method call or an assignment, refcount goes down by one.
  4. The instance's refcount is zero, so it's destroyed. All non-database link resources are closed.
  5. You attempt to access a closed resource, returning 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

Sign up to request clarification or add additional context in comments.

2 Comments

In this case it was for a rather simple chainable OOP structure. Consider SqlCommand::create()->select('foo')->from('bar')->where('foo.id = 3')->orderBy('foo.id ASC')->fetchRow(). This works even though I technically haven't created an instance of the SqlCommand first. This also works for scalar values already, just not resources for whatever reason, so what's the reason it works for scalar values? Surely they don't increase/decrease the refcount any differently just because my last property is a scalar value rather than a resource?
Resources are a special exception. Database handles are an exception to the special exceptions. I'll edit my post.
2

It seems that it's all about variable scoping.

In short if you assign Foo::create() to a global variable you can access the handle 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.

5 Comments

There's still something wacky going on though. If you set handle to a scalar in the constructor, and then set it to null in the destructor, you'll get back the expected pre-nulled value when you reference it. This unsetting behavior only occurs when handle is a resource. I think it's a bug. If $foo = new Foo(); echo $foo->bar; behaves differently than echo (new Foo())->bar; then it makes the latter notation pretty useless.
Oh - it gets worse - just doing some further tinkering and now I'm even more confused; I'll update this...
Heh, it surely is. I didn't find anything in the bug tracker for it either.
Really great thoughts and tests - thank you so much for tinkering around with it. I thought it might be a bug too, but surely I can't be the only one to have made this rather simple code, and considering this bug has been in PHP for almost 10 years it just seems unlikely that no one else would've run into the same issue. Maybe they have and just couldn't be bothered figuring out why, though. I'm also no OPCODE person at all, but the only difference is ASSIGN !0, $1. Works - doesn't work
Also note, while HHVM appears to work because it outputs "stream" instead of "unknown" -- you can't actually use the stream. It will fail with a warning about invalid stream resource. So it's consistent at least. :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.