2 заметки с тегом

файл

Способы хранения временных данных в PHP

PHP позволяет хранить временные данные в ОЗУ или файлах. Это реализовано с помощью потоков чтения-записи php://memory и php://temp, которые можно открыть через fopen() и управлять данными в файлоподобной обёртке до завершения скрипта. После окончания работы PHP уничтожит все временные данные, если их намеренно не сохранить. Куда записать временные данные:

Способ                 Описание
————————————————————————————————————————————————————————————————————————————————————
php://memory           Записывает и всегда хранит данные в ОЗУ. С помощью fopen()
                       можно работать как с обычным файлом. Сохранить данные можно
                       копированием в другой поток stream_copy_to_stream() или
                       передачей контента stream_get_contents() в другой обработчик.

php://temp             Создаёт файл во временной папке, когда размер данных
                       превышает 2 Мбайт. До этого все записанные данные
                       хранятся в ОЗУ. Временная папка определяется аналогично
                       функции sys_get_temp_dir().

php://temp/maxmemory:0 Обнуляет максимальный размер данных в байтах для хранения
                       в памяти и с первым байтом создаёт файл во временной папке.
                       Можно установить свой лимит стартового хранения данных в
                       ОЗУ путем добавления /maxmemory:NN, где NN — это
                       максимальный размер данных в байтах для хранения в памяти
                       перед использованием временного файла.

tmpfile()              Создаёт временный файл с уникальным именем в режиме
                       чтения и записи r+, а затем возвращает файловый указатель.
                       Ничем не отличается от php://temp/maxmemory:0, кроме
                       способа вызова. URI временного файла можно получить
                       с помощью функции stream_get_meta_data() и сохранить данные
                       через копирование файла copy() по URI.

Все временные файлы автоматически удаляются, а ОЗУ очищается после завершения скрипта. php://memory и php://temp нельзя будет переиспользовать, т. к. после закрытия потоков невозможно сослаться на них. Как правило, сохранность временных данных, по необходимости, достигается путём копирования. Ниже привожу два примера, которые демонстрируют работу со временными данными:

<?php

///////
// 1 //
///////

// Откроем поток php://memory
$temp_data = fopen('php://memory', 'w+');

// Запишем "Hello, world!"
fwrite($temp_data, 'Hello, world!');

// Извлечём метаданные из потока
stream_get_meta_data($temp_data);

// Array
// (
//     [timed_out]    => false
//     [blocked]      => true
//     [eof]          => false
//     [wrapper_type] => PHP
//     [stream_type]  => MEMORY
//     [mode]         => w+b
//     [unread_bytes] => 0
//     [seekable]     => true
//     [uri]          => php://memory
// )

// Сбросим курсор
rewind($temp_data);

// Получим "Hello, world!"
stream_get_contents($temp_data);

// Создадим новый файл data.txt
$fh = fopen('data.txt', 'w+');

// Сбросим курсор ещё раз
rewind($temp_data);

// Сохраним временные данные в новый файл
stream_copy_to_stream($temp_data, $fh);

// Закроем ресурс файла data.txt
fclose($fh);

// Закрывать поток php://memory необязательно, т. к. все данные
// будут автоматически уничтожены по завершению скрипта.
// fclose($temp_data);


///////
// 2 //
///////

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

// Запишем "Hello, world!"
fwrite($tmpfile, 'Hello, world!');

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

// Array
// (
//     [timed_out]    => false
//     [blocked]      => true
//     [eof]          => false
//     [wrapper_type] => plainfile
//     [stream_type]  => STDIO
//     [mode]         => r+b
//     [unread_bytes] => 0
//     [seekable]     => true
//     [uri]          => home\user\temp\phpDC08.tmp
// )

// Получаем URI временного файла
$uri = stream_get_meta_data($tmpfile)['uri'];

// Сохраним "Hello, world!" в файл data.txt
copy($uri, 'data.txt');

// Временный файл будет автоматически удалён по завершению
// скрипта. Закрывать открытый дескриптор необязательно.
// fclose($tmpfile);
8 мая   php   tmpfile   данные   файл

UploadedUrl — пишем класс для загрузки удалённых файлов на PHP

Задача. Написать класс для загрузки удалённых файлов по URL. Использовать для решения задачи пакет Symfony HttpFoundation. Класс должен иметь настройки и корректно обрабатывать ошибки. Объяснить устройство класса и показать примеры использования.

В Symfony HttpFoundation существует класс UploadedFile, который загружает файлы из $_FILES. Он умеет сохранять файл на диске и безопасно определять MIME-тип. Нам нужно создать класс с такой же функциональностью, но для загрузки удалённых файлов по URL. Свой класс мы назовём UploadedUrl, как бы расширяя Symfony HttpFoundation, и наследуем класс File, чтобы повторить все возможности UploadedFile. Структура нашего проекта будет выглядеть следующим образом:

┌src/
├──Exception/
│  └──UrlException.php
├──UploadedUrl.php
├──tmpfile.php
└──cacert.pem

Файл UploadedUrl.php будет содержать всю логику нашего класса, а остальные будут решать вспомогательные задачи: UrlException.php — класс для обработки исключений, tmpfile.php — класс для работы с временным файлом, cacert.pem — сертификат для безопасного соединения. Для тех, кто хочет сразу посмотреть результат, может скачать код с репозитория denisyukphp/uploaded-url или установить через Composer.

Для загрузки удалённого файла мы задействуем cURL, который скачает весь контент во временный файл, а затем передадим его URI в конструктор класса File. После завершения работы временный файл будет автоматически удалён. Подключить tmpfile.php можно через Composer с моего репозитория denisyukphp/tmpfile. Почему он лучше функции tmpfile() я рассказал на Хабре.

Перед началом написания класса нам необходимо определиться с интерфейсом инициализации. Предлагаю UploadedUrl принимать два аргумента: 1) http:// или https:// ссылку; 2) необязательный массив с настройками. В итоге вот так должен выглядеть запуск класса:

// обычная загрузка удалённого файла
new UploadedUrl('');

// загрузка файла с настройками по умолчанию
new UploadedUrl('', [
    'verify'     => true,
    'cert'       => 'cacert.pem',
    'maxsize'    => null,
    'timeout'    => null,
    'useragent'  => null,
    'buffersize' => 1024,
    'redirects'  => true,
    'maxredirs'  => 10,
    'curl'       => [],
]);

Подробнее о параметрах настроек поговорим чуть ниже, но этих будет вполне достаточно для полноценной работы. В целом схему работы класса можно представить в виде цепочки: получение URL и настроекинициализация и подготовка cURLзапрос и сохранения данных во временный файлобработка ошибок. Далее я буду небольшими блоками строить класс и по действиям в сниппетах кода давать разъяснения.

Посмотреть 258 строк кода UploadedUrl с комментариями
///////////////////////////////////
// 5 // Как пользоваться классом //
///////////////////////////////////

Для быстрого старта достаточно объявить класс new UploadedUrl('') с валидной ссылкой в первом аргументе. После чего с удалённым файлом можно работать на своём сервере. Файл будет находится во временной папке до конца работы PHP и автоматически удалится, если его не переместить в другое место. Ниже привожу примеры использования UploadedUrl с настройками и моими комментариями:

<?php

$file = new Denisyuk\UploadedUrl('http://i.imgur.com/JRorA8V.gif', [

    // Безопасное соединение. Если установить false, то
    // cURL отключит значения CURLOPT_SSL_VERIFYPEER и
    // CURLOPT_SSL_VERIFYHOST.
    'verify' => true,

    // Можно указать директорию с PEM-сертификатами или отдельный файл.
    // По умолчанию используется cacert.pem, свежию версию которого
    // можно скачать на https://curl.haxx.se/docs/caextract.html
    // или самостоятельно настроить автообновление свежего
    // сертификата по ссылке https://curl.haxx.se/ca/cacert.pem
    'cert' => __DIR__,

    // Зададим максимальный размер загружаемого файла
    // в байтах (по умолчанию без ограничений).
    'maxsize' => 1024 * 1024 * 32,

    // Максимальное время выполнения запроса.
    'timeout' => 60,

    // Информация о пользователе (если null, то будет
    // взято из $_SERVER['HTTP_USER_AGENT']).
    'useragent' => null,

    // Это количество байт, через которое cURL будет
    // сверять размер загружаемого файла. В нашем случае,
    // через каждый килобайт будет выполняться ф-я для
    // проверки размера скачанных данных. Для больших
    // размеров установите значение на 2% меньше
    // от разрешённого (для 1Гб установите 1024 * 1024 * 2).
    'buffersize' => 1024,

    // Разрешить следовать перенаправлениям (по умолчанию 10).
    // cURL будет переходит по ссылкам, где HTTP-ответ
    // имеет заголовок с Location.
    'redirects' => true,

    // Количество перенаправлений. Лучше установить
    // больше двух, т. к. возможны перенаправления
    // с HTTP на HTTPS.
    'maxredirs' => 5,

    // другие CURLOPT_* опции
    'curl' => [
        CURLOPT_AUTOREFERER    => true,
        CURLOPT_CONNECTTIMEOUT => 10,
        /* ... */
    ];
]);

Поскольку UploadedUrl будет наследовать класс File, то он также вернёт набор методов класса SplFileInfo. Это расширяет возможности нашего класса. В общем мы имеем мощный инструмент для работы с файлом. Давайте посмотрим какая функциональность нам доступна:

// получить MIME-тип, который можно
// безопасно использовать для валидации
// определённых файлов
$file->getMimeType();

// получить расширение файла
$file->guessExtension();

// переместить файл в другую папку
$file->move(__DIR__ . '/data', 'foobar.jpg');

// получить размер файла
$file->getSize();

// Остальные доступные методы для SplFileInfo можно
// посмотреть на http://php.net/manual/ru/class.splfileinfo.php
/////////////////////////////////////////
// 6 // Выявляем и обрабатываем ошибки //
/////////////////////////////////////////

UploadedUrl сам не бросает исключение в случае ошибок при загрузке файла. Для отслеживания этого процесса можно использовать три метода: isValid() — проверит были ли ошибки в cURL; getErrorMessage() — вернёт сообщение об ошибке, которое можно показать пользователю; getErrorCode() — вернёт код ошибки cURL (можно использовать для написания сообщений об ошибках на своём языке). Как это всё работает показано ниже:

<?php

use Denisyuk\UploadedUrl;
use Denisyuk\Exception\UrlException;

try {
    // (1) ссылка на загружаемый файл
    $url = 'http://windows.php.net/downloads/releases/php-7.1.1-src.zip';

    // (2) загрузим свежую версию PHP, которая весит ~26 Мбайт
    $file = new UploadedUrl($url, [

        // (3) укажем максимальный размер файла 5 Мбайт
        'maxsize' => 1024 * 1024 * 5,
    ]);

    // (4) проверяем на ошибки
    if (!$file->isValid()) {

        // (5) бросаем исключение
        throw new UrlException(
            $file->getErrorMessage(),
            $file->getErrorCode()
        );

        // Здесь мы можем сформировать свои названия
        // ошибок исходя из getErrorCode(), т. к. это
        // код ошибки cURL. Например:
        //
        // $code = $file->getErrorCode();
        // $errors = [
        //     /*  1 */ CURLE_UNSUPPORTED_PROTOCOL => '',
        //     /*  6 */ CURLE_COULDNT_RESOLVE_HOST => '',
        //     /* 22 */ CURLE_HTTP_RETURNED_ERROR  => '',
        //     /* 42 */ CURLE_ABORTED_BY_CALLBACK  => '',
        // ];
        // $message = isset($errors[$code]) ? $errors[$code] : '';
        //
        // Также мы можем использовать свойства класса
        // $file->url и $file->maxsize для написания
        // более красивых названий своим ошибкам.
        //
        // throw new UrlException($message, $code);
    }

    // (6) работаем дальше, если нет ошибок
    echo $file->getMimeType();

// (7) ловим исключение
} catch(UrlException $e) {

    // (8) выводим сообщение об ошибке
    // выведет: 'Файл "php-7.1.1-src.zip" не должен превышать 5 Мбайт.'
    echo $e->getMessage();
}

Я дальше планирую поддерживать и развивать этот класс на denisyukphp/uploaded-url как дополнение к Symfony HttpFoundation. Если у вас есть какие-либо замечания или предложния, то можете отправить их мне на почту a@denisyuk.by или Вконтакте. Буду очень благодарен за любую обратную связь, которая поможет улучшить UploadedUrl. В ближайшее время намереваюсь нормально оформить проект на Гитхабе и добавить несколько косметических улучшений.