После долгих поисков я наконец понял, что происходит в моем коде, поэтому этот вопрос не «как мне это исправить«, скорее «почему это происходит?».
Рассмотрим следующий код:
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);
}
}
Кажется достаточно простым — при создании он откроет поток памяти и установит свойство $handle
а также $id
, При разрушении будет использовать fclose
закрыть этот поток.
использование:
$foo = Foo::create(1); // works
var_dump( $foo->bar()->handle ); // works
var_dump( Foo::create(2)->bar()->handle ); // doesn't work
Проблема здесь в том, что я ожидаю, что оба вызова будут возвращаться точно так же, но по какой-то причине Foo::create(2)
позвони, где я не сохранить экземпляр в переменной вызывает сборщик мусора где-то между return $this
часть bar()
метод и я на самом деле, используя свойство $handle
,
Если вам интересно, этот это выход:
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;
Из того, что я вижу, это то, что происходит:
var_dump( Foo::create(2)->bar()->handle );
// run GC before continuing.. ^^ .. but I'm not done with it :(
Но Зачем? Почему PHP думает, что я закончил с экземпляром переменной / класса и, следовательно, чувствует необходимость его уничтожить?
демос:
eval.in demo
3v4l демо (Только HHVM может понять это — все другие версии PHP не могут)
Это все сводится к refcounts и как относится к PHP ресурсы по-разному.
Когда экземпляр класса уничтожается, все ресурсы, не связанные с базой данных, закрываются (см. Ссылку на ресурсы выше). Все не-ресурсы, на которые есть ссылки в другом месте, будут действительны.
В вашем первом примере вы назначаете $temp = Foo::create(1)
что увеличивает пересчет на экземпляр Foo
, предотвращая его уничтожение, что делает ресурс открытым.
Во втором примере var_dump( Foo::create(2)->bar()->handle );
вот как все складывается:
Foo::create(2)
называется, создавая экземпляр Foo
,bar()
на новом экземпляре, возвращая $this
что увеличивает счет на один.bar()
Область действия и следующее действие не являются вызовом метода или назначением, refcount уменьшается на единицу.Unknown
,Как дополнительное доказательство, это прекрасно работает:
$temp = Foo::create(3)->bar();
// $temp keep's Foo::create(3)'s refcount above zero
var_dump( $temp->handle );
Как это сделать:
$temp = Foo::create(4)->bar()->bar()->bar();
// Same as previous example
var_dump( $temp->handle );
И это:
// 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 );
это не Работа:
$temp = Foo::create(6)->handle;
// Nothing has a reference to Foo, it gets destroyed, all resources closed.
var_dump($temp);
Ни один не делает это:
$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);
когда Foo
уничтожен, все открытые ресурсы (кроме ссылок на базы данных) закрыты. Ссылки на другие свойства от Foo
все еще действительны.
Демонстрации:
https://eval.in/271514
Кажется, все дело в переменной области видимости.
Короче если назначить
Foo::create()
к глобальной переменной вы можете
получить доступ кhandle
в глобальном масштабе и деструктор не будет
вызывается до конца сценария.Принимая во внимание, что если вы на самом деле не назначаете его глобальной переменной, то последняя
вызов метода в локальной области вызовет деструктор; рукоять
закрыт вFoo::create(1)->bar()
так->method
сейчас закрыт, когда
вы пытаетесь получить к нему доступ.
Дальнейшее расследование показывает, что предпосылка ошибочна — здесь определенно что-то странное происходит! Это только кажется, влияет на ресурсы.
Случай 1
$foo = Foo::create(1);
var_dump( $foo->bar()->handle );
Результаты в:
resource(3) of type (stream)
В этом случае мы присвоили глобальную переменную $foo
быть новым экземпляром Foo
создан с Foo::create(1)
, Теперь мы обращаемся к этой глобальной переменной с bar()
вернуть себя, а затем общественности handle
,
случай 2
$bar = Foo::create(2)->bar();
var_dump( $bar->handle );
Результаты в:
resource(4) of type (stream)
Опять все нормально, потому что Foo::create(2)
создал новый экземпляр Foo
а также bar()
просто вернул его (он все еще имел доступ к нему в локальной области видимости). Это было присвоено глобальной переменной $bar
и это от того, что handle
восстанавливается.
случай 3
var_dump( Foo::create(3)->bar()->handle );
Результаты в:
resource(5) of type (Unknown)
Это потому что когда Foo::create()
возвращает новый экземпляр Foo
используется bar()
… однако когда bar()
закрывается там больше нет местный Использование этого экземпляра и тому __destruct()
вызывается метод, который закрывает дескриптор. Это тот же результат, который вы получили бы, если бы просто написали:
$h = fopen('php://memory', 'r+');
fclose($h);
var_dump($h);
Вы получите точно такой же результат, если попытаетесь:
var_dump( Foo::create(3)->handle );
Foo::create(3)
вызовет деструктор, потому что больше нет локальных вызовов к этому экземпляру.
РЕДАКТИРОВАТЬ
Дальнейшее возрождение еще больше замутило воды …
Я добавил этот метод:
public function handle() {
return $this->handle;
}
Теперь, если моя предпосылка была правильной, делаю:
var_dump( Foo::create(3)->handle() );
должен привели к:
resource(3) of type (stream)
… но это не так, снова вы получаете тип ресурса неизвестный — кажется деструктор вызывается в return $this
до общедоступный член класса доступен! Тем не менее, совершенно нормально вызывать метод:
public function handle() {
return $this->bar();
}
Это с радостью вернет вам ваш объект:
object(Foo)#1 (2) {
["id":"Foo":private]=>
int(3)
["handle"]=>
resource(3) of type (stream)
}
Кажется, нет способа получить доступ к членам класса ресурсов таким образом, прежде чем вызывается деструктор ?!
Как Алекс Хованский указывает, что со скалярами все в порядке:
public function __destruct() {
$this->id = 2000;
fclose($this->handle);
}
public function handle() {
return $this->id;
}
Сейчас:
var_dump( Foo::create(3)->handle() );
Результаты в:
int(3)
… исходный $ id был возвращен до вызова деструктора.
Это определенно пахнет как ошибка для меня.