- PVSM.RU - https://www.pvsm.ru -

Как создать временный файл на PHP, когда функция tmpfile() не подходит

Когда PHP-программисту необходимо создать временный файл он в мануале находит функцию tmpfile() и после изучения примеров начинает думать как её лучше применить. Так было и со мной, когда мне потребовалось выгрузить данные сразу во временный файл, а не работать с ними через переменную. Но с файлом, созданным таким образом, в дальнейшем неудобно работать в силу того, что tmpfile() возвращает дескриптор, а не ссылку на локальный файл. Давайте немного углубимся в анатомию временного файла и рассмотрим подводные камни, с которыми мне пришлось столкнуться.

Функция tmpfile() создаёт ресурс, так как это делает fopen(), и работает с потоками ввода-вывода STDIO [1]. Это эквивалентно тому, если бы мы открыли поток php://temp для последующей работы с временным файлом. В обоих случаях файл появится во временной папке, которая прописана в php.ini, и будет автоматически удалён по завершению скрипта или досрочно с помощью fclose().

При работе с php://temp файл будет создан во временной папке когда размер данных перевалит за 2 Мбайт. До этого все записанные данные будут храниться в php://memory. Это ограничение можно обойти, если сразу войти в поток php://temp/maxmemory:0. — PHP [2]

Поскольку tmpfile() и fopen() при создании временного файла работают с потоками, мы можем с помощью stream_get_meta_data() извлечь метаданные и узнать реальный путь к файлу для дальнейших манипуляций:

<?php

// Создаём временный файл
$tmpfile = tmpfile();

// Извлекаем метаданные из потока
$data = stream_get_meta_data($tmpfile);

/* ... */

Какие значения возвращает stream_get_meta_data() хорошо описано в документации [3], но нас больше интересует имя файла, связанное с потоком. Его можно извлечь по ключу uri в массиве.

Array
(
    [timed_out]    => false
    [blocked]      => true
    [eof]          => false
    [wrapper_type] => plainfile
    [stream_type]  => STDIO
    [mode]         => r+b
    [unread_bytes] => 0
    [seekable]     => true
    [uri]          => homeusertempphpDC08.tmp
)

В случае с php://temp мы никак не сможем получить URI из метаданных, хотя файл по факту будет создан во временной папке, если его вес превысит 2 Мбайт. Другого способа узнать где физически хранится временный файл и под каким именем при работе с потоками не существует.

Реальный путь к файлу может понадобиться, когда возникнет необходимость переместить временный файл в другое место на диске, т. е. сохранить таким образом записанные в него данные. Сделать это с помощью rename() нельзя, поскольку tmpfile() накладывает блокирующий режим на поток и отменить его через stream_set_blocking(), как показала практика, не получится. Конечно же решить проблему можно копированием, но тогда до окончания работы скрипта у нас будет две копии данных, если не позаботиться о досрочном удалении временного файла.

Перекидывать ресурс из одного объекта в другой тоже не очень удобно, потому что для данной реализации понадобится интерфейс. В моём случае нужно было передать строго ссылку на временный файл в объект, который в дальнейшем должен работать с файлом без каких-либо ограничений. На этом этапе стало понятно, что функция tmpfile() для создания временного файла не подходит.

Для альтернативного решения мне потребовалось написать свой механизм, который работал бы по следующей схеме: создание файла во временной папкелюбые манипуляции с файломавтоматическое удаление. Создать файл с уникальным именем во временной папке PHP позволяет с помощью функции tempnam().

<?php

// Создаст файл во временной папке
$tmpfile = tempnam(sys_get_temp_dir(), 'php');

/* ... */

Первым аргументом указывается расположение временной папки через sys_get_temp_dir(), а вторым — префикс в имени файла. Такой файл доступен для чтения и записи только владельцу, т. к. создаётся с правами 0600 (rw-). Для реализации автоматического удаления файла предлагаю перенести дальнейшую логику в класс, где с помощью __destruct() попробуем удалить файл.

<?php

class tmpfile
{
    public $filename;

    public function __construct()
    {
        $this->filename = tempnam(sys_get_temp_dir(), 'php');
    }

    public function __destruct()
    {
        @unlink($this->filename);
    }

    public function __toString()
    {
        return $this->filename;
    }
}

// Создаём временный файл
$tmpfile = new tmpfile;

// Работаем как с обычным файлом
file_put_contents($tmpfile, 'Hello, world!');

/* ... */

Объект вернёт ссылку на файл, который создала функция tempnam(), т. к. в классе прописан __toString(). Таким образом мы избавились от работы с ресурсом. Сам файл будет удалён при освобождении всех ссылок на объект или по завершению скрипта, но до того случая, пока не будет вызвана фатальная ошибка или брошено исключение.

Деструктор вызывается при уничтожении объекта. В случае критических ошибок __destruct() может не вызваться в PHP7 и ниже. Деструктор не должен оставлять объект в нестабильном состоянии. Поэтому в PHP обработчики уничтожения и освобождения объекта отделены друг от друга. Обработчик освобождения вызывается, когда движок полностью уверен в том, что объект больше нигде не применяется. — Объекты в PHP7 [4]

Удалять файл через деструктор не лучшая практика, которая, кстати, применяется во многих доступных решениях [5]. Для гарантированного удаления файла мы можем зарегистрировать свою функцию, которая выполнится в любом случае после завершения скрипта. Делается это с помощью register_shutdown_function() в конструкторе нашего класса:

<?php

class tmpfile
{
    public $filename;

    public function __construct()
    {
        $this->filename = tempnam(sys_get_temp_dir(), 'php');

        register_shutdown_function(function () {
            @unlink($this->filename);
        });
    }

    public function __toString()
    {
        return $this->filename;
    }
}

/* ... */

Такой подход позволяет создать временный файл без использования tmpfile() или php://temp, что в ООП очень удобно. Стандартные способы предпочтительнее для решения локальных задач, где вся логика инкапсулирована в одном методе или классе.

В итоге получился класс для работы с временным файлом. Исходники я выложил в репозитории на Гитхабе image denisyukphp/tmpfile [6] и добавил в класс поддержку CRUD-операций. Методы для записи и чтения являются обёртками для file_put_contents() и file_get_contents(). Подключить в свой проект можно через Composer.

Посмотреть примеры

<?php

require __DIR__ . '/vendor/autoload.php';

// Создать временный файл
$tmpfile = new tmpfile;

// Записать в файл
$tmpfile->write('Hello, world!');

// Прочитать часть файла
$tmpfile->read(7, 5);

// Передать имя файла в объект
new SplFileInfo($tmpfile);

// Переместить в другую папку
rename($tmpfile, __DIR__ . '/data.txt');

// Досрочно удалить временный файл
$tmpfile->delete();

/* ... */

Репозиторий на Github [6]
Проект на Packagist [7]

Автор: denisyukphp

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/php-2/236810

Ссылки в тексте:

[1] STDIO: https://ru.wikipedia.org/wiki/Stdio.h

[2] PHP: http://php.net/manual/ru/wrappers.php.php#refsect2-wrappers.php-unknown-unknown-unknown-unknown-unknown-descriptios

[3] документации: http://php.net/manual/ru/function.stream-get-meta-data.php

[4] Объекты в PHP7: https://habrahabr.ru/company/mailru/blog/275497/

[5] решениях: https://packagist.org/search/?q=temp%20file

[6] denisyukphp/tmpfile: https://github.com/denisyukphp/tmpfile

[7] Проект на Packagist: https://packagist.org/packages/denisyukphp/tmpfile

[8] Источник: https://habrahabr.ru/post/320078/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best