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. В ближайшее время намереваюсь нормально оформить проект на Гитхабе и добавить несколько косметических улучшений.

Поделиться
Отправить
Популярное