Задача. Написать класс для загрузки удалённых файлов по URL. Использовать для решения задачи пакет Symfony HttpFoundation. Класс должен иметь настройки и корректно обрабатывать ошибки. Объяснить устройство класса и показать примеры использования.
В Symfony HttpFoundation существует класс UploadedFile, который загружает файлы из $_FILES. Он умеет сохранять файл на диске и безопасно определять MIME-тип. Нам нужно создать класс с такой же функциональностью, но для загрузки удалённых файлов по URL. Свой класс мы назовём UploadedUrl, как бы расширяя Symfony HttpFoundation, и наследуем класс File, чтобы повторить все возможности UploadedFile. Структура нашего проекта будет выглядеть следующим образом:
Файл 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) необязательный массив с настройками. В итоге вот так должен выглядеть запуск класса:
Подробнее о параметрах настроек поговорим чуть ниже, но этих будет вполне достаточно для полноценной работы. В целом схему работы класса можно представить в виде цепочки: получение 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 (можно использовать для написания сообщений об ошибках на своём языке). Как это всё работает показано ниже:
<?phpuse 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) бросаем исключениеthrownew 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. В ближайшее время намереваюсь нормально оформить проект на Гитхабе и добавить несколько косметических улучшений.
Два месяца назад я перешёл на PHP7 и в этом посте хочу поделиться нововведениями, которые пришлись по нраву мне больше всего. Во первых, переход на новую ветку PHP обусловлен приростом производительности и уменьшением потребляемых ресурсов, т. е. крупные сайты с PHP7 могут в два раза снизить расходы на хостинг и быстрее генерировать веб-страницу конечному пользователю. Здесь выигрывают сразу и владельцы сайтов, и их посетители.
Это сравнение скорости между версиями 5.6 и 7.0 на различных платформах. Zend Framework даёт прирост скорости на 133%, а WordPress — 129%. Такое улучшение PHP имеет позитивные последствия для бизнеса.
В прошлом году Badoo, WordPress.com, Tumblr и многие другие гиганты перешли на PHP7. Так, например, компании Badoo за счёт снижения CPU в два раза и уменьшения потребления памяти в 8 раз удалось сэкономить 1 млн долларов. Сайт WordPress.com также полностью переведён на PHP7, где прирост производительности действительно заметен:
Быстродействие в новых версиях стало самым крутым и обсуждаемым нововведением. Теперь не нужно заморачиваться по оптимизации каких-то участков кода, а сразу писать понятный и поддерживаемый код. Во вторых, в PHP 7.0 и 7.1 введено большое количество улучшений на уровне синтаксиса и функциональности. Далее на примерах я покажу, что мне понравилось в новых версиях и как эти нововведения поменяют язык PHP в будущем на уровне кода.
Замена тернарного оператора
На замену тернарного оператора пришёл «оператор объединения с null» или «null-коалесцентный оператор». Через два знака вопроса (??) можно указать цепочку значений в одну строку и будет выбрано первое значение, которое не равно null. Раньше этот подход применялся вместе с функцией isset(). Сейчас можно указать даже не существующие переменные и не мучатся c большой вложенностью как с тернарным оператором.
Начиная с версии 7.0 появилась возможность группировать объявления импорта классов, функций и констант, находящиеся в одном пространстве имён, в одной строке с помощью оператора use. Это нововведение на уровне синтаксического сахара, которое может наделить объявление имён каких-то компонентов определённой логикой.
<?php// PHP 5.6use App\Bundle\Foo;
use App\Bundle\Bar;
use App\Bundle\Baz as B;
// PHP 7+use App\Bundle\{Foo, Bar, Baz as B};
// илиuse App\Bundle\{
Foo,
Bar,
Baz as B
};
/* ... */// PHP 5.6useconstapp\namespace\ru_RU;
useconstapp\namespace\en_US;
usefunctionapp\namespace\firstFunc;
usefunctionapp\namespace\secondFunc;
// PHP 7+useconstapp\namespace\{ru_RU, en_US};
usefunctionapp\namespace\{firstFunc, secondFunc};
Это самое мощное нововведение для ООП. Теперь при объявлении метода для каждой переменной можно указать свой тип, а также тип данных, который вернёт этот метод. В PHP 7.0 доступны следующие типы: array, callable, bool, float, int, string, имя класса или интерфейса. В версии 7.1 добавили ещё void и iterable. Тип void можно использовать только в возвращаемых значениях функций, которые ничего не возвращают. Псевдо-тип iterable используется в качестве значения массива или объекта, реализующего интерфейс Traversable.
В примере выше отработала принуждающая декларация типов, т. е. PHP динамически привёл значения к нужному типу, когда в переменную с объявленным типом int мы передали string, например. Вместо того, чтобы сгенерировать ошибку, все значения переменных и результат функции был преобразован к указанному типу на лету. Это поведение установлено по умолчанию.
Строгую типизацию можно включить, если в начале скрипта прописать declare(strict_types=1). Все значения должны полностью соответствовать указанным типам. В противном случае будет сгенерирована ошибка TypeError. Строгая типизация не затрагивает весь остальной код и её нужно прописывать для каждого скрипта отдельно.
<?php// включили строгую типизациюdeclare(strict_types=1);
classFoo
{
publicstaticfunctionbar(int $a, int $b) : int
{
return$a + $b;
}
}
// вернёт ошибку TypeError, т. к.// первый аргумент является string,// а должен intvar_dump( Foo::bar('1', 2) );
// вернёт int(3)var_dump( Foo::bar(1, 2) );
В версии 7.1 появилась возможность обнулить возвращаемые типы. Это расширяет список возвращаеммых типов до null. Теперь функция может вернуть какой-то явно указанный тип или null. Достигается такое поведение путём добавления префикса в виде знака вопроса к указанному типу:
Декларация типов и возвращаемых значений выводит PHP на новый уровень и эти нововведения мне нравятся больше всего, т. к. со стороны разработчика повышается читабельность кода; чёткая согласованность входных и выходных данных; программист может быстрее понять, чего ожидать от функции; легче документировать код; появляется возможность изменять типы данных на лету. Это основополагающая функциональность PHP7, которой не знать уже сейчас недопустимо.
В PHP 7.0 появился новый класс для внутренних ошибок Error и интерфейс исключений Throwable. Теперь Error и старый класс Excetion реализуют Throwable (пользовательские классы не могут реализовывать данный интерфейс). Exception можно использовать для отлова исключений, которые будут обработаны и выполнение программы продолжится. Класс Error служит для необратимых исключений и выбрасывается в случае ошибки PHP или на уровне ошибок разработчиков.
Большинство ошибок уровня E_ERROR или E_RECOVERABLE_ERROR будут выбрасывать Error, но некоторые будут выбрасывать объекты подклассов: ArithmeticError, AssertionError, ParseError, DivisionByZeroError, TypeError и ArgumentCountError (с версии 7.1). Эти подклассы ошибок не могут быть брошены самостоятельно, а только лишь словлены. Иерархию всех исключений можно представить в виде дерева:
Начиная с версии 7.1 появилась возможность в блоке catch обрабатывать сразу несколько исключений, перечисляя их через символ вертикальной черты. Может быть полезно для обработки одинаковых по логике ошибок или пользовательских исключений.
<?phptry {
/* * код программы */
} catch (
ParseError |
TypeError |
ArgumentCountError $e
) {
/* * ловим три вида ошибок PHP в одном блоке */
} catch (AppException | UserException $e) {
/* * ловим пользовательские исключения */
} catch (Exception $e) {
/* * ловим остальные исключения */
} catch (Throwable $t) {
/* * ловим и логируем все ошибки и исключения */
}
На место фатальных ошибок пришли исключения, которые могут быть обработаны и позволяют корректно завершить программу в случае каких-то косяков. Это добавляет гибкости PHP-приложению.
Нововведения в PHP7+ это лишь начало большого перехода на новый уровень языка. С каждым релизом приложения будут становиться быстрее и безопаснее. На версии 7.0 и 7.1 нужно отреагировать уже сегодня и будь в курсе новых тенденций PHP, дабы не выпадать из сферы. Ниже привожу одним списком остальные нововведения:
7.0 Оператор spaceship (космический корабль) ↩ 7.0 Задание констант массивов с помощью define()↩ 7.0 Появились анонимные классы ↩ 7.0 Синтаксис кодирования Unicode ↩ 7.0 Добавлен метод в замыкания Closure::call()↩ 7.0unserialize() с фильтрацией ↩ 7.0 Новый класс IntlChar↩ 7.0 Улучшена старая функция assert()↩ 7.0 Выражение return в генераторах ↩ 7.0 Делегация генератора с помощью конструкции yield from↩ 7.0 Новая функция intdiv() для целочисленного деления ↩ 7.0session_start() принимает массив опций ↩ 7.0 Новая функция preg_replace_callback_array()↩ 7.0 Новые функции random_bytes() и random_int()↩ 7.0list() может раскрывать объекты реализующие ArrayAccess↩ 7.0 Обращение к методам и свойствам класса при клонировании (clone $foo)->bar()↩ 7.1 Короткий синтаксис для list()↩ 7.1 Публичные и приватные константы классов ↩ 7.1 Поддержка ключей в list()↩ 7.1 Поддержка отрицательных смещений для строк ↩ 7.1 Расширены функции openssl_encrypt() и openssl_decrypt()↩ 7.1 Преобразование callable в Closure с помощью Closure::fromCallable()↩ 7.1 Новая функция pcntl_async_signals()↩ 7.1 Поддержка Server Push в CURL 7.46 ↩
В этой статье подробно разберём механизм загрузки изображений на сервер с помощью PHP не прибегая к сторонним компонентам и фреймворкам. Научимся безопасно загружать изображения не только с локальной машины пользователя, но и удалённые файлы по ссылке. Все примеры кода я буду писать в процедурном стиле, дабы вы быстрее могли читать код, а не перескакивать с одного метода на другой. Руководство полностью авторское и не претендует на какую-либо академичность изложения.
Всю последовательность загрузки изображения на сервер можно отобразить следующим образом: настройка php.ini → получение файла → проверка безопасности → валидация данных → сохранение на диск. Процесс загрузки картинки с компьютера пользователя или по URL ничем не отличаются, за исключением способа получения изображения и его сохранения. Общая схема загрузки картинки на сервер выглядит следующим образом:
Для валидации картинки по URL мы будем использовать функцию getimagesizefromstring(), т. к. cURL скачает её в переменную для дальнейших манипуляций.
Поскольку мы загружаем изображения на сервер, то хорошо было бы проверять их определённые параметры: ширину, высоту, тип картинки, размер файла в байтах. Это зависит от логики вашего приложения, но для наглядности в этом руководстве мы проверим все вышеописанные параметры.
§2. Правила безопасности
Безопасность загрузки изображений сводится к недопущению попадания на сервер чужеродного кода и его выполнения. На практике загрузка картинок наиболее уязвимое место в PHP-приложениях: попадание shell-скриптов, запись вредоносного кода в бинарные файлы, подмена EXIF-данных. Для того, чтобы избежать большинства методов взлома нужно придерживаться следующих правил:
ане доверять данным из $_FILES; бне проверять MIME-тип картинки из функции getimagesize(); в загружаемому файлу генерировать новое имя и расширение; г запретить выполнение PHP-скриптов в папке с картинками; дне вставлять пользовательские данные через require и include; е для $_FILES использовать is_uploaded_file() и move_uploaded_file().
Если есть чем дополнить «Правила безопасности», тогда оставляйте свои замечания или ссылки на статьи по безопасности в комментариях к этому руководству, а я опубликую их в этом параграфе.
PHP позволяет внести определённые конфигурационные значения в процесс загрузки любых файлов. Для этого необходимо в файле php.ini найти блоки «Resource Limits», «Data Handling» и «File Uploads», а затем отредактировать, по необходимости, следующие значения:
[Resource Limits]; Максимальное время выполнения скрипта в секундахmax_execution_time=60; Максимальное потребление памяти одним скриптомmemory_limit=64M[Data Handling]; Максимально допустимый размер данных отправляемых методом POSTpost_max_size=5M[File Uploads]; Разрешение на загрузку файловfile_uploads=On; Папка для хранения файлов во время загрузкиupload_tmp_dir=home/user/temp; Максимальный размер загружаемого файлаupload_max_filesize=5M; Максимально разрешённое количество одновременно загружаемых файловmax_file_uploads=10
Исходя из указанных значений, пользователь не сможет за один раз загрузить больше десяти файлов, причём каждый файл не должен превышать 5 Мбайт. Параметры из блока «Resource Limits» больше нужны для загрузки удалённого файла, т. к. с помощью cURL мы будем скачивать содержимое в переменную и проверять её по нужным нам критериям, а для этого необходимо дополнительное время и память.
Конфигурационный файл php.ini всегда необходимо настраивать согласно бизнес-логики разрабатываемого веб-приложения. Например, мы планируем загружать не более десяти файлов до 5 Мбайт, а это значит нам понадобиться ~50 Мбайт памяти. Кроме того, нам нужно знать максимальное время загрузки одного файла с локальной машины и по ссылке, дабы установить достаточное время выполнения скрипта в max_execution_time и не пугать пользователей ошибками.
§4. Загрузка картинок из формы
Сейчас мы не будем рассматривать загрузку нескольких файлов на сервер, а разберём лишь саму механику загрузки на примере одного файла. Итак, для загрузки картинки с компьютера пользователя необходимо с помощью HTML-формы отправить файл PHP-скрипту методом POST и указать способ кодирования данных enctype="multipart/form-data" (в данном случае данные не кодируются и это значение применяется только для отправки бинарных файлов). С формой ниже мы будем работать дальше:
Для поля выбора файла мы используем имя name="upload" в нашей HTML-форме, хотя оно может быть любым. После отправки файла PHP-скрипту file-handler.php его можно перехватить с помощью суперглобальной переменной $_FILES['upload'] с таким же именем, которая в массиве содержит информацию о файле:
Array
(
[name] => picture.jpg// оригинальное имя файла[type] => image/jpeg// MIME-тип файла[tmp_name] => home\user\temp\phpD07E.tmp// бинарный файл[error] => 0// код ошибки[size] => 17170// размер файла в байтах
)
Не всем данным из $_FILES можно доверять: MIME-тип и размер файла можно подделать, т. к. они формируются из HTTP-ответа, а расширению в имени файла не стоит доверять в силу того, что за ним может скрываться совершенно другой файл. Тем не менее, дальше нам нужно проверить корректно ли загрузился наш файл и загрузился ли он вообще. Для этого необходимо проверить ошибки в $_FILES['upload']['error'] и удостовериться, что файл загружен методом POST с помощью функции is_uploaded_file(). Если что-то идёт не по плану, значит выводим ошибку на экран.
<?php// Перезапишем переменные для удобства$filePath=$_FILES['upload']['tmp_name'];
$errorCode=$_FILES['upload']['error'];
// Проверим на ошибкиif ($errorCode!== UPLOAD_ERR_OK ||!is_uploaded_file($filePath)) {
// Массив с названиями ошибок$errorMessages= [
UPLOAD_ERR_INI_SIZE =>'Размер файла превысил значение upload_max_filesize в конфигурации PHP.',
UPLOAD_ERR_FORM_SIZE =>'Размер загружаемого файла превысил значение MAX_FILE_SIZE в HTML-форме.',
UPLOAD_ERR_PARTIAL =>'Загружаемый файл был получен только частично.',
UPLOAD_ERR_NO_FILE =>'Файл не был загружен.',
UPLOAD_ERR_NO_TMP_DIR =>'Отсутствует временная папка.',
UPLOAD_ERR_CANT_WRITE =>'Не удалось записать файл на диск.',
UPLOAD_ERR_EXTENSION =>'PHP-расширение остановило загрузку файла.',
];
// Зададим неизвестную ошибку$unknownMessage='При загрузке файла произошла неизвестная ошибка.';
// Если в массиве нет кода ошибки, скажем, что ошибка неизвестна$outputMessage=isset($errorMessages[$errorCode]) ?$errorMessages[$errorCode] :$unknownMessage;
// Выведем название ошибкиdie($outputMessage);
}
Для того, чтобы злоумышленник не загрузил вредоносный код встроенный в изображение, нельзя доверять функции getimagesize(), которая также возвращает MIME-тип. Функция ожидает, что первый аргумент является ссылкой на корректный файл изображения. Определить настоящий MIME-тип картинки можно через расширение FileInfo. Код ниже проверит наличие ключевого слова image в типе нашего загружаемого файла и если его не окажется, выдаст ошибку:
// Создадим ресурс FileInfo$fi=finfo_open(FILEINFO_MIME_TYPE);
// Получим MIME-тип$mime= (string) finfo_file($fi, $filePath);
// Проверим ключевое слово image (image/jpeg, image/png и т. д.)if (strpos($mime, 'image') ===false) die('Можно загружать только изображения.');
На данном этапе мы уже можем загружать абсолютно любые картинки на наш сервер, прошедшие проверку на MIME-тип, но для загрузки изображений по определённым характеристикам нам необходимо валидировать их с помощью функции getimagesize(), которой скормим сам бинарный файл $_FILES['upload']['tmp_name']. В результате мы получим массив максимум из 7 элементов:
Для дальнейшей валидации изображения и работы над ним нам необходиом знать только 3 значения: ширину, высоту и размер файла (для вычисления размера применим функцию filesize() для бинарного файла из временной папки).
// Результат функции запишем в переменную$image=getimagesize($filePath);
// Зададим ограничения для картинок$limitBytes=1024*1024*5;
$limitWidth=1280;
$limitHeight=768;
// Проверим нужные параметрыif (filesize($filePath) >$limitBytes) die('Размер изображения не должен превышать 5 Мбайт.');
if ($image[1] >$limitHeight) die('Высота изображения не должна превышать 768 точек.');
if ($image[0] >$limitWidth) die('Ширина изображения не должна превышать 1280 точек.');
После всех проверок мы можем с уверенностью переместить наш загружаемый файл в какую-нибудь папку с картинками. Делать лучше это через функцию move_uploaded_file(), которая работает в безопасном режиме. Перед перемещением файла нельзя забыть сгенерировать случайное имя и расширение из типа изображения для нашего файла. Вот так это выглядит:
// Сгенерируем новое имя файла на основе MD5-хеша$name=md5_file($filePath);
// Сгенерируем расширение файла на основе типа картинки$extension=image_type_to_extension($image[2]);
// Сократим .jpeg до .jpg$format=str_replace('jpeg', 'jpg', $extension);
// Переместим картинку с новым именем и расширением в папку /picsif (!move_uploaded_file($filePath, __DIR__ .'/pics/'.$name.$format)) {
die('При записи изображения на диск произошла ошибка.');
}
На этом загрузка изображения завершена. Для более удобной загрузки файлов можете использовать класс UploadedFile из пакета Symfony HttpFoundation, который является обёрткой для $_FILES и также сохраняет файл через move_uploaded_file().
Для загрузки изображения по ссылке нам понадобиться библиотека cURL, которая работает с удалёнными ресурсами. С помощью неё мы скачаем контент в переменную. С одной стороны может показаться, что для этих целей подойдёт file_get_contents(), но на самом деле мы не сможем контролировать объём скачиваемых данных и нормально обрабатывать все возникшие ошибки. Для того, чтобы cURL корректно скачал данные нам нужно: разрешить следовать перенаправлениям, включить проверку сертификата, указать максимальное время работы cURL (формируется за счёт объёма скачиваемых данных и средней скорости работы с ресурсом). Как правильно скачать файл в переменную показано ниже с необходимыми параметрами:
<?php// Каким-то образом получим ссылку$url='https://site.ru/picture.jpg';
// Проверим HTTP в адресе ссылкиif (!preg_match("/^https?:/i", $url) &&filter_var($url, FILTER_VALIDATE_URL)) {
die('Укажите корректную ссылку на удалённый файл.');
}
// Запустим cURL с нашей ссылкой$ch=curl_init($url);
// Укажем настройки для cURLcurl_setopt_array($ch, [
// Укажем максимальное время работы cURL
CURLOPT_TIMEOUT =>60,
// Разрешим следовать перенаправлениям
CURLOPT_FOLLOWLOCATION =>1,
// Разрешим результат писать в переменную
CURLOPT_RETURNTRANSFER =>1,
// Включим индикатор загрузки данных
CURLOPT_NOPROGRESS =>0,
// Укажем размер буфера 1 Кбайт
CURLOPT_BUFFERSIZE =>1024,
// Напишем функцию для подсчёта скачанных данных// Подробнее: http://stackoverflow.com/a/17642638
CURLOPT_PROGRESSFUNCTION =>function ($ch, $dwnldSize, $dwnld, $upldSize, $upld) {
// Когда будет скачано больше 5 Мбайт, cURL прервёт работуif ($dwnld>1024*1024*5) {
return-1;
}
},
// Включим проверку сертификата (по умолчанию)
CURLOPT_SSL_VERIFYPEER =>1,
// Проверим имя сертификата и его совпадение с указанным хостом (по умолчанию)
CURLOPT_SSL_VERIFYHOST =>2,
// Укажем сертификат проверки// Скачать: https://curl.haxx.se/docs/caextract.html
CURLOPT_CAINFO => __DIR__ .'/cacert.pem',
]);
$raw=curl_exec($ch); // Скачаем данные в переменную$info=curl_getinfo($ch); // Получим информацию об операции$error=curl_errno($ch); // Запишем код последней ошибки// Завершим сеанс cURLcurl_close($ch);
Если всё прошло успешно и cURL уложился в 60 секунд, тогда содержимое по ссылке будет скачано в переменную $raw. Кроме того, функция curl_getinfo() вернёт информацию о проделанном запросе, откуда мы можем получить дополнительную информацию для анализа работы с удалёнными ресурсами:
Array
(
[content_type] => image/jpeg// MIME-тип из Content-Type[http_code] => 200// последний HTTP-код[redirect_count] => 0// количество перенаправлений[total_time] => 0.656// общее время работы cURL[connect_time] => 0.188// время на соединение с хостом[size_download] => 4504// реальный размер полученных данных[download_content_length] => 4504// размер данных из Content-Length/* ... */
)
Дальше нам нужно проверить нет ли ошибок в curl_errno() и удостовериться, что ресурс отдаёт HTTP-код равный 200, иначе мы скажем, что по такому-то URL ничего не найдено. После всех проверок переменную $raw передаём в getimagesizefromstring() и работаем уже по отработанной схеме как в случае с загрузкой картинок из формы.
Обратите внимание, что мы валидируем размер файла в момент получения данных, т. к. мы не можем на 100% доверять curl_getinfo(), поскольку значения content_type, http_code, download_content_length формируются на основе полученных HTTP-заголовков. Скачивать файл полностью, а потом проверять количество байт потребует много времени и памяти. Поэтому мы контролировали размер получаемых данных с помощью опции CURLOPT_PROGRESSFUNCTION: как только cURL получит больше данных, чем наш лимит, он прекратит работу и выдаст ошибку CURLE_ABORTED_BY_CALLBACK.
// Проверим ошибки cURL и доступность файлаif ($error=== CURLE_OPERATION_TIMEDOUT) die('Превышен лимит ожидания.');
if ($error=== CURLE_ABORTED_BY_CALLBACK) die('Размер не должен превышать 5 Мбайт.');
if ($info['http_code'] !==200) die('Файл не доступен.');
// Создадим ресурс FileInfo$fi=finfo_open(FILEINFO_MIME_TYPE);
// Получим MIME-тип используя содержимое $raw$mime= (string) finfo_buffer($fi, $raw);
// Закроем ресурс FileInfofinfo_close($fi);
// Проверим ключевое слово image (image/jpeg, image/png и т. д.)if (strpos($mime, 'image') ===false) die('Можно загружать только изображения.');
// Возьмём данные изображения из его содержимого$image= getimagesizefromstring($raw);
// Зададим ограничения для картинок$limitWidth=1280;
$limitHeight=768;
// Проверим нужные параметрыif ($image[1] >$limitHeight) die('Высота изображения не должна превышать 768 точек.');
if ($image[0] >$limitWidth) die('Ширина изображения не должна превышать 1280 точек.');
// Сгенерируем новое имя из MD5-хеша изображения$name=md5($raw);
// Сгенерируем расширение файла на основе типа картинки$extension=image_type_to_extension($image[2]);
// Сократим .jpeg до .jpg$format=str_replace('jpeg', 'jpg', $extension);
// Сохраним картинку с новым именем и расширением в папку /picsif (!file_put_contents(__DIR__ .'/pics/'.$name.$format, $raw)) {
die('При сохранении изображения на диск произошла ошибка.');
}
Для сохранения изображения на диск можно воспользоваться file_put_contents(), которая запишет контент в файл. Новое имя файла мы создадим через функцию md5(), а расширение сделаем из image_type_to_extension(). Теперь мы можем загружать любые картинки по ссылке.
В этом параграфе разберём способы загрузки нескольких изображений за один раз с локальной машины пользователя и по удалённым ссылкам. Для отправки ссылок мы задействуем $_POST и передадим ей все данные с помощью тега textarea. Для загрузки файлов из формы мы продолжим дальше работать с $_FILES. Наша новая HTML-форма будет немного отличаться от старой.
В конец имени поля выбора файла name="upload[]" добавились фигурные скобки и аттрибут multiple, который разрешает браузеру выбрать несколько файлов. Все файлы снова загрузятся во временную папку, если не будет никаких ошибок в php.ini. Перехватить их можно в $_FILES, но на этот раз суперглобальная переменная будет иметь неудобную структуру для обработки данных в массиве. Решается эта задача небольшими манипуляциями с массивом:
<?php// Изменим структуру $_FILESforeach($_FILES['upload'] as$key=>$value) {
foreach($valueas$k=>$v) {
$_FILES['upload'][$k][$key] =$v;
}
// Удалим старые ключиunset($_FILES['upload'][$key]);
}
// Загружаем все картинки по порядкуforeach ($_FILES['upload'] as$k=>$v) {
// Загружаем по одному файлу$_FILES['upload'][$k]['tmp_name'];
$_FILES['upload'][$k]['error'];
}
Для загрузки нескольких картинок по URL передадим наши ссылки через textarea с именем name="upload", где их можно указать через пробел или с новой строки. Функция preg_split разберёт все данные из $_POST['upload'] и сформирует массив, по которому нужно пройтись циклом и каждый валидный URL отправить в обработчик.
$data=preg_split("/\s+/", $_POST['upload'], -1, PREG_SPLIT_NO_EMPTY);
foreach ($dataas$url) {
// Валидируем и загружаем картинку по URL
}
Вы можете улучшить форму для загрузки изображений, например, воспользоваться библиотекой FineUploader или jQuery FileUpload, чтобы настроить выбор картинок с определённым расширением.
Важные замечания:
(1) в файле php.ini значения post_max_size и upload_max_filesize должны совпадать;
(2) до валидации изображения стоит говорить, что мы работаем с файлом;
(3) используйте класс UploadedFile для $_FILES в боевых условиях;
(4) ошибку UPLOAD_ERR_FORM_SIZE можно словить, если в форме указать MAX_FILE_SIZE;
(5) библиотека FastImage работает быстрее getimagesize();
(6) для имитации браузера в cURL нужно установить CURLOPT_USERAGENT;
(7) ограничить редиректы можно через CURLOPT_MAXREDIRS.
Для разработки сайтов программисту почти всегда нужны тестовые данные. Иногда бывает так, что данные нужны в большом количестве и разного качества. Я поделюсь небольшим рецептом заполнения БД любой информацией на PHP за 5 минут. В качестве основного генератора данных мы будем использовать библиотеку Faker, которая поддерживает ru_RU локализацию. Для доступа к БД я возьму RedBeanPHP — это простая ORM-библиотека, позволяющая быстро вставлять данные налету. Такая комбинация инструментов хороша когда данные не обязательно должны быть уникальными и программисту, возможно, ещё до конца неизвестна структура БД.
Сам по себе процесс наполнения базы ни в коем случае не должен влиять на бизнес-логику приложения. Внося данные в базу нужно пропускать их через модели своего приложения, чтобы исключить конфликтные ситуации.
Мы специально опустим это правило в целях демонстрации процесса и сэкономим немного времени, чтобы пройтись по ключевым возможностям Faker и RedBeanPHP. Предположим программисту нужно быстро запостить фейковые данные о пользователях. Ничего сложного, но давайте для начала установим наши библиотеки. Сделаем это через Composer:
После установки пакетов нам нужен обработчик. Предлагаю создать обычный PHP-файл и написать там всю логику. Мне кажется это самый простой способ продемонстрировать работоспособность метода. Ниже привожу листинг с комментариями, где указано как подключить наши библиотеки в скрипте.
<?php// Подключим автозагрузку классовrequire'vendor/autoload.php';
// Объявим неймспейсыuse Faker\Factory as Faker;
use RedBeanPHP\R as RedBean;
// Запишем экземпляр класса Faker\Factory в переменную$faker= Faker::create('ru_RU');
// Подключаемся к БД как в PDO
RedBean::setup('mysql:host=localhost;dbname=faker', 'root', '');
/* ... */
Когда всё подключено, можно приступить к генерации данных. Поскольку мы создали экземпляр класса Faker\Factory через статический метод create('ru_RU') с русской локализацией, то теперь все сгенерированные данные для наших фейковых пользователей будут на русском языке. Библиотека предлагает достаточно много генераторов данных, но давайте посмотрим как их получить для нашего примера.
<?php/* ... */// Создадим ассоциативный массив, где// укажем рандомные данные о пользователе// с помощью Faker$data= [
'login'=>$faker->userName,
'password'=> password_hash($faker->md5, PASSWORD_DEFAULT),
'firstName'=>$faker->firstNameMale,
'lastName'=>$faker->lastName,
'email'=>$faker->email,
];
Это произвольные данные для примера, но вы можете сотавить более сложные согласно своей задаче используя все возможности Faker. Теперь эти данные нужно пропустить через свою модель в приложении и сохранить. В качестве такой у нас выступает RedBeanPHP без различных валидаций и прочей логики. Допустим у нас в БД ещё нет таблицы users и её нужно заполнить. RedBeanPHP позволяет добавлять данные налету, т. е. нет необходимости заранее создавать структуру таблиц. Она сама создаст таблицу со всеми нужными полями. Это основное преимещество RedBeanPHP, которое ускоряет работу с БД. На примере это выглядит так:
<?php/* ... */// Создаём молель с именем users$user= RedBean::dispense('users');
// В стиле ActiveRecord присваиваем наши значенияforeach ($dataas$key=>$value) {
$user->{$key} =$value;
}
// Сохраняем модель в БД
RedBean::store($user);
Если нужно добавить сразу несколько пользователей в БД, тогда оберните всё логику в цикл for и укажите количество итераций. На этом весь рецепт быстрого добавления данных в БД заканчивается. По существу мы не написали ни одного SQL-запроса, даже не открыли PhpMyAdmin, но на выходе получили заполненую таблицу с нужными нам полями. Вот такую таблицу сгенерировал RedBeanPHP:
Для чтения или модификации данных можно использовать RedBeanPHP, т. к. он заточен под CRUD-операции, умеет работать с множественными таблицами, интегрируется в свои модели как полноценная ORM. Этого достаточно для быстрого доступа к БД и вывода данных.