Пишу, по большей части, про историю, свою жизнь и немного про программирование.

SplFileObject

ПХП, как известно, вырос из процедурного языка — во многих местах до сих пор видны «уши» процедурного подхода. Одно из таких мест — работа с файлами. Большинство ребят используют тут исключительно процедурный подход. Что-то вроде:

$fp = @fopen("test.log", "ab");
if ($fp !== false) {
    // в реальном коде надо бы проверить результат этих функций,
    // тут упрощённый код для иллюстрации принципа
    flock($fp, LOCK_EX);
    fwrite($fp, "Log line\n");
    fclose($fp);
} else {
    throw new \RuntimeException(error_get_last()['message']);
}

К счастью с 2005 года в языке существует класс SplFileObject, который умеет делать тоже самое, но в ООП-стиле.

Единственный его недостаток — у него нет операции закрытия файла. Видимо предлагается полагаться на сборку мусора, что не очень хорошо, ведь у блокирующего процесса сборка может затянуться в силу каких-то причин и блокируемый будет ждать непозволительно долго. Я думаю это проблему вполне можно решать ручным снятием блокировки:

$fp = new \SplObjectFile("test.log", "ab");
$fp->flock(LOCK_EX);
$fp->fwrite("Log line\n");
$fp->flock(LOCK_UN);

Интересно, что когда я пытался продемонстрировать проблему с блокировками, то довольно быстро накидал код, в котором она происходила:

$x = [
    new \SplFileObject('test.log', 'ab'),
    &$x
];

$x[0]->flock(LOCK_EX);
unset($x);

echo "Written\n";
sleep(10);

Если попробовать этот код запустить в двух разных консолях, то второй процесс будет ждать первый. По крайней мере по «МакОС» и ПХП 7.4.

Но, если заменить unset на $x = null, произойдёт удивительное — второй процесс получит возможность записать в файл сразу же, то есть сборщик мусора убирает его моментально. Я не знал о таком различии между этими двумя способами уничтожения объекта. Любопытно — с чем это связано и изменится ли в будущем?

Добавлено позднее: я догадался почему такой эффект, попробуйте догадаться тоже.

5 комментариев
aktuba 2020

Так unset($x) и $x = null не равноценны-же. Даже в 5-ке, насколько помню, так-же.
Первый — удаляет объект, обнуляет ссылку. Второй делает только последнее.

Евгений Степанищев 2020

Не понял, что значит «удаляет объект»?

aktuba 2020

https://3v4l.org/EDQEn/vld#output вот тут можно увидеть оп-коды, в них видна (3-я и 7-я строки) разница в работе unset vs =null.
https://github.com/php/php-src/blob/12fec7a14ef28a8193e4563cb9ce71d81ada56c0/Zend/zend_API.c#L3924 — тут (не уверен, что именно это место, даже если нет — сути не меняет) видна разная реализация, которая позволяет прикинуть что и как...

Евгений Степанищев 2020

Не понимаю чем тут помогут опкоды.

aktuba 2020

Комменты не удобные ((

Евгений Степанищев 2020

В чём неудобство?

Andrii Kasian 2020

unset($x) в вашем коде удаляет массив, а не объект. Это то, что вы хотели написать?

Евгений Степанищев 2020

Да.

Nikita Popov 2020

Перенесу комментарий Никиты Попова сюда:

Hi,

Nice example ;) The difference is this:

$x = [new \SplFileObject(’test.log’, ’ab’), &$x];
unset($x);

unset() here will unset the variable $x, but not look through the reference that’s stored inside. So you are still left with a self-referencing array, which will be collected later by the cycle collector. We say that unset() (and the =& operator) are «reference-breaking» for this reason.

$x = [new \SplFileObject(’test.log’, ’ab’), &$x];
$x = null;

The simple assignment looks through references. So both the variable $x will become null, and the reference to $x inside the array. You are left with [SplFileObject, null], which doesn’t have a cycle and is immediately destroyed.

In real code (where the cycle will usually be through objects, not references in arrays), you will not see a difference between unset() or $x = null.