# Weird destructor call order on stream wrappers

- Thread: https://externals.io/message/101987
- Original archive: https://news-web.php.net/php.internals/101987
- Started by Dennis Birkholz on 2018-03-20T22:13:38+00:00
- Messages: 5
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #101987 — Dennis Birkholz — 2018-03-20T22:13:38+00:00

URL: https://externals.io/message/101987#101987

Hello together,

today I noticed that the order in which stream_flush/stream_close and
the destructor of a (userspace implemented) stream wrapper are called is
weird:

When a stream is closed with `fclose()`, the call order is as expected:
stream_flush, stream_close, finally the destructor.
But if the stream is closed automatically on program exit, the order
seems wrong: stream_flush and stream_close are called AFTER the
destructor was called (and so the object should be destroyed!)

The documentation (http://php.net/manual/en/streamwrapper.destruct.php)
for the destructor contains the ominous sentence "Called when closing
the stream wrapper, right before streamWrapper::stream_flush()." which
is clearly only half of the truth.

Example: https://3v4l.org/CMItP

Is this some kind of "desired" behavior? Should I open a bug?

Greets,
Dennis

P.s.:
In addition to the call order, in auto-close case autoloading with
registered autoloaders does not work inside stream_flush and
stream_close, it seems registered autoloaders (maybe all objects) have
been dispatched when the calls happen, see: https://3v4l.org/GB7Mm

(I also appended both example files as I don't know how long snippets at
3v4l.org persist)

---

## #101988 — Dennis Birkholz — 2018-03-20T22:20:08+00:00

URL: https://externals.io/message/101987#101988 · In reply to: #101987

Hello again,

Am 20.03.2018 um 23:13 schrieb Dennis Birkholz:
> (I also appended both example files as I don't know how long snippets at
> 3v4l.org persist)

the list does not allow attachments, so inline. If this does not work,
3v4l must suffice.

Greets,
Dennis

<?php

\spl_autoload_register(function($class) {
    echo "Autoloader for '$class' called\n";
    class_alias(Test::class, $class);
    return true;
});

class Test
{
    static function blubb()
    {
        echo static::class, "\n";
    }
}

class MyStreamWrapper
{
    static $counter = 1;

    private $id;

    public function __construct()
    {
        $this->id = self::$counter++;
    }

    public function stream_open($path, $mode, $options, &$opened_path)
    {
        echo "({$this->id}) stream_open()\n";
        return true;
    }

    public function stream_write($data)
    {
        echo "({$this->id}) stream_write()\n";
        return \strlen($data);
    }

    public function stream_flush()
    {
        echo "({$this->id}) stream_flush()\n";

        if ($this->id === 1) {
            Lalala::blubb();
        } else {
            Foobar::blubb();
        }

        return true;
    }

    public function stream_close()
    {
        echo "({$this->id}) stream_close()\n";
        return true;
    }

    public function __destruct()
    {
        echo "({$this->id}) __destruct()\n";
    }
}

\stream_wrapper_register('test', MyStreamWrapper::class);

if (($handle = \fopen('test:///foobar', 'r+')) === false) {
    throw new \RuntimeException("Failed to open file");
}

if (\fwrite($handle, 'test') === false) {
    throw new \RuntimeException("Failed to write to file");
}

if (\fclose($handle) === false) {
    throw new \RuntimeException("Failed to close file");
}

if (($handle2 = \fopen('test:///blubb', 'r+')) === false) {
    throw new \RuntimeException("Failed to open file");
}

if (\fwrite($handle2, 'test') === false) {
    throw new \RuntimeException("Failed to write to file");
}

---

## #101989 — Rowan Collins — 2018-03-20T23:14:37+00:00

URL: https://externals.io/message/101987#101989 · In reply to: #101987

> P.s.:
> In addition to the call order, in auto-close case autoloading with
> registered autoloaders does not work inside stream_flush and
> stream_close, it seems registered autoloaders (maybe all objects) have
> been dispatched when the calls happen, see: https://3v4l.org/GB7Mm

The behaviour with the stream wrapper certainly seems wrong to me, since 
there is clearly still a reference to the object after its destructor is 
called.

The autoloader not being available seems reasonable, though - during 
shutdown, the engine has to start unpicking various global state in 
order to cleanly release any references, and there is no order to do 
this that would guarantee no code breaking. You demonstrate a stream 
wrapper relying on an autoloader, but we could equally have an 
autoloader that relied on a stream wrapper. If both needed to be 
available for the other, the engine could never cleanly free any 
resources they held, so it has to start somewhere. In general, anything 
that might run on shutdown, such as a destructor, should be very 
conservative in its assumptions about the engine's state.

Regards,

-- 
Rowan Collins
[IMSoP]

---

## #101994 — me@jhdxr.com — 2018-03-22T05:59:37+00:00

URL: https://externals.io/message/101987#101994 · In reply to: #101987

There is a related bug report: https://bugs.php.net/bug.php?id=75931 


It also point out that the construcor method has been ignored.


Best regards,
CHU Zhaowei

---

## #101998 — Dennis Birkholz — 2018-03-22T15:10:34+00:00

URL: https://externals.io/message/101987#101998 · In reply to: #101994

Hello,

Am 22.03.2018 um 06:59 schrieb CHU Zhaowei:
> There is a related bug report: https://bugs.php.net/bug.php?id=75931 

Thank you for pointing out this bug report. It is about stream filters
(extending the php_user_filter class) and not about stream wrappers
which seem to have similar issues.

> It also point out that the construcor method has been ignored.

That does not apply to stream wrappers, the constructor is called just fine.

Greets
Dennis
