13 заметок с тегом

php

Ctrl + ↑ Позднее

Учимся работать с API Яндекс.Метрики

Для извлечения статистических данных из Яндекс.Метрики необходимо воспользоваться специальным разделом API очётов. Сейчас актуальна версия v1. В качестве примеров, для получения статистики посещений всего сайта и конкретной страницы, мы выгрузим табличные данные в формате JSON, а затем преобразуем их и построим два линейных графика. Ниже привожу базовую структуру GET-запроса, с которым мы будем работать:

https://api-metrika.yandex.ru/stat/v1/data ? ids=<int>
                                           & oauth_token=<string>
                                           & metrics=<string>
                                           & dimensions=<string>
                                           & filters=<string>
                                           & date1=<string>
                                           & date2=<string>
                                           & sort=<string>

Яндекс.Метрика оперирует двумя базовыми сущностями: метрики (metrics) и группировки (dimensions). Метрики определяют числовые величины, которые могут быть рассчитаны на основе сессии или хита пользователя [количество посетителей, процент отказов, глубина просмотра, …]. Группировки предназначены для выборки данных по определённым признакам [страница входа, источник трафика, дата анализа, …].

Яндекс.Метрика работает отдельно с визитами (ym:s), где s — session, и хитами (ym:pv), где pv — pageview. В запросах к API эти характеристики нельзя использовать вместе в метриках и группировках. Список всех метрик и группировок можно посмотреть в документации.

Для детальных запросов существуют фильтры сегментации (filters). Они рассчитывают результат по отдельному сегменту данных. Например, когда нужно получить какие-либо данные только из поисковых систем или вытянуть статистику посещений по конкретной странице. С помощью параметров date1 и date2 можно задать отчётный период, а sort необходим для сортировки данных по признаку из dimensions. Небольшая шпаргалка ниже:

Параметр    Описание
——————————————————————————————————————————————————
ids         Номер счётчика.
oauth_token Токен авторизации.
metrics     Список метрик через запятую.
dimensions  Список группировок через запятую.
filters     Фильтры сегментации с условием.
date1       Дата начала периода выборки.
date2       Дата окончания периода выборки.
sort        Сортировка по метрикам и группировкам.

Для начала нужно зарегистрировать своё приложение на oauth.yandex.ru и получить oauth_token. В качестве ids выступает номер вашего счётчика (его можно узнать на главной странице Яндекс.Метрики). Параметры ids, oauth_token и metrics являются обязательными в простых запросах, но если применяется filters, то metrics и dimensions можно не указывать.

Предлагаю перейти в плоскость PHP и сформировать первый боевой запрос на примере моего блога, который получит количество визитов, просмотров и уникальных посетителей на всём сайте за последнюю неделю:

<?php

$url = 'https://api-metrika.yandex.ru/stat/v1/data';

$params = [
    'ids'         => '14446750',
    'oauth_token' => 'Z4AAAAAYZjVkAAQP-FYZ6SJdSkhesN0MJ0dXOZo',
    'metrics'     => 'ym:s:visits,ym:s:pageviews,ym:s:users',
    'dimensions'  => 'ym:s:date',
    'date1'       => '7daysAgo',
    'date2'       => 'yesterday',
    'sort'        => 'ym:s:date',
];

echo file_get_contents( $url . '?' . http_build_query($params) );

Как видите, в metrics и dimensions мы указали значения для визитов (ym:s). Это значит, что данные будут сформированы в рамках сессии по каждому пользователю. Если посетитель перезагрузит одну и туже страницу сто раз, то ym:s:pageviews не будет меняться. В случае работы с хитами (ym:pv), значение ym:pv:pageviews увеличилось бы на сто. Хиты собирают статистику в рамках каждого просмотра страницы.

Дату для отчётного периода можно указать в формате YYYY-MM-DD или её относительное значение: today, yesterday, 7daysAgo (номер дня можно заменить на свой). В результате Яндекс.Метрика вернула следующие данные в формате JSON:

{
  "query" : {
    "ids" : [ 14446750 ],
    "dimensions" : [ "ym:s:date" ],
    "metrics" : [ "ym:s:visits", "ym:s:pageviews", "ym:s:users" ],
    "sort" : [ "ym:s:date" ],
    "date1" : "2017-02-14",
    "date2" : "2017-02-20",
    "limit" : 100,
    "offset" : 1,
    "group" : "Week",
    "auto_group_size" : "1",
    "quantile" : "50",
    "attribution" : "Last",
    "currency" : "RUB"
  },
  "data" : [ {
    "dimensions" : [ {
      "name" : "2017-02-14"
    } ],
    "metrics" : [ 437.0, 800.0, 376.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-15"
    } ],
    "metrics" : [ 390.0, 559.0, 322.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-16"
    } ],
    "metrics" : [ 243.0, 370.0, 196.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-17"
    } ],
    "metrics" : [ 168.0, 274.0, 131.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-18"
    } ],
    "metrics" : [ 111.0, 145.0, 75.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-19"
    } ],
    "metrics" : [ 121.0, 150.0, 87.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-20"
    } ],
    "metrics" : [ 132.0, 251.0, 103.0 ]
  } ],
  "total_rows" : 7,
  "total_rows_rounded" : false,
  "sampled" : false,
  "sample_share" : 1.0,
  "sample_size" : 1602,
  "sample_space" : 1602,
  "data_lag" : 74,
  "totals" : [ 1602.0, 2549.0, 988.0 ],
  "min" : [ 111.0, 145.0, 75.0 ],
  "max" : [ 437.0, 800.0, 376.0 ]
}

Секция query содержит параметры запроса, но нас больше интересует data, в которой содержатся наши статистические данные. Секция data представляет собой массив, где итеративно заполнены поля dimensions и metrics теми данными, которые были указаны в запросе. В нашем случае, поле dimensions содержит name — это название группировки ym:s:date. Поле metrics — массив с числовыми данными: ym:s:visits, ym:s:pageviews и ym:s:users.

Для обработки общих статистических данных можно использовать такие секции как: totals, min и max (в некоторых отчётах удобно анализировать сразу обобщённую информацию). Чтобы вывести данные из секции data на экран, скормим их Highcharts и построим линейный график:

Посмотреть процесс создания графика: PHP + Highcharts

Мы получили статистику по визитам, просмотрам и уникальным посетителям на всех страницах моего блога за последнюю неделю. Выглядит довольно таки неплохо. Для получения статистики конкретной страницы в Яндекс.Метрике нужно использовать filter. Вот так будет выглядеть запрос и построение графика на amCharts для просмотра главной страницы моего блога по хитам (ym:pv):

<?php

/////////////////////////////////////////////////////////
///// ПАРАМЕТРЫ GET-ЗАПРОСА ДЛЯ КОНКРЕТНОЙ СТРАНИЦЫ /////
/////////////////////////////////////////////////////////
$url = 'https://api-metrika.yandex.ru/stat/v1/data';

$params = [
    'ids'         => '14446750',
    'oauth_token' => 'Z4AAAAAYZjVkAAQP-FYZ6SJdSkhesN0MJ0dXOZo',
    'metrics'     => 'ym:pv:pageviews',
    'dimensions'  => 'ym:pv:date',
    'filters'     => rawurldecode("ym:pv:URL=='https://denisyuk.by/'"),
    'date1'       => '30daysAgo',
    'date2'       => 'yesterday',
    'sort'        => 'ym:pv:date',
];

/////////////////////////////////////////////////////////
///// ПОЛУЧИМ ДАННЫЕ В JSON И ПЕРЕВЕДЁМ ИХ В МАССИВ /////
/////////////////////////////////////////////////////////
$json = file_get_contents( $url . '?' . http_build_query($params) );
$data = json_decode($json, true)['data'];

/////////////////////////////////////////////////////////////
///// ПРЕОБРАЗУЕМ ДАННЫЕ ДЛЯ ЛИНЕЙНОГО ГРАФИКА AMCHARTS /////
/////////////////////////////////////////////////////////////
$tmpdata = [];

foreach ($data as $item) {
    $tmpdata[] = [
        'category' => $item['dimensions'][0]['name'],
        'column-1' => $item['metrics'][0],
    ];
}

////////////////////////////////////
///// ВЕРНЁМ JSON ДЛЯ AMCHARTS /////
////////////////////////////////////
$dataProvider = json_encode($tmpdata);

///////////////////////////////////////////////////////
///// ВЫВЕДЕМ HTML-КОД И ПОСТРОИМ ГРАФИК AMCHARTS /////
///////////////////////////////////////////////////////
echo <<<HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://www.amcharts.com/lib/3/amcharts.js"></script>
  <script src="https://www.amcharts.com/lib/3/serial.js"></script>
  <script src="https://www.amcharts.com/lib/3/themes/patterns.js"></script>
  <script>
    AmCharts.makeChart("amcharts-container", {
      "type": "serial",
      "categoryField": "category",
      "startDuration": 1,
      "theme": "patterns",
      "categoryAxis": {
        "gridPosition": "start"
      },
      "graphs": [{
        "balloonText": "[[title]] за [[category]] — [[value]]",
        "bullet": "round",
        "title": "Просмотры",
        "id": "AmGraph-1",
        "type": "smoothedLine",
        "valueField": "column-1"
      }],
      "valueAxes": [{
        "title": "Количество"
      }],
      "titles": [{
        "text": "Просмотры https://denisyuk.by за последние 30 дней"
      }],
      "dataProvider": $dataProvider
    });
  </script>
</head>
<body>
  <div id="amcharts-container" style="max-width: 720px; height: 400px;"></div>
</body>
</html>
HTML;
//end

Обратите внимание, что значение filters мы обернули в функцию rawurldecode(). Это нужно делать, когда вы работаете со сложными фильтрами сегментации и кириллицей в запросах. Другие возможности фильтра доступны в документации: можно делать множественную выборку, использовать регулярные выражения, задействовать дополнительные операторы и др. В итоге мы получили график amCharts для статистики просмотров главной страницы по хитам:

Обновлено 23 Фев 2017: Решил добавить круговую диаграмму на Highcharts, чтобы разнообразить пост (под графиком сниппет кода для построения диаграммы). С помощью группировки ym:s:<attribution>TrafficSource и метрики ym:s:visits мы получили статистику по всем источникам трафика, а через фильтр сегментации, указав значение ym:s:visits>10, скрыли источники трафика меньше десяти, чтобы не портить график.

Посмотреть процесс создания круговой диаграммы: PHP + Highcharts

Мы рассмотрели базовые возможности API Яндекс.Метрики, которые могут пригодится для построения отчётов посещаемости своего сайта. Здесь важно понять, что все запросы формируются на основе metrics и dimensions, а через filters вычленяются конкретные данные по сайту. Если у вас остались вопросы, то задавайте их в комментариях, а я, по мере необходимости, буду редактировать или дополнять пост.

Продолжить обучение:

    Документация API очётов Яндекс.Метрики
    Становясь гуру API Яндекс.Метрики
    Получаем статистические данные, используя API Yandex Metrika
    Яндекс.Метрика для Laravel 5

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

Путеводитель по нововведениям PHP 7.0 и 7.1

Два месяца назад я перешёл на 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 большой вложенностью как с тернарным оператором.

<?php

// PHP 5.6
$test = isset($foo) ? $foo : null;

// PHP 7+
$test = $foo ?? null;

/* ... */

// PHP 5.6
$test = isset($foo) ? $foo : (isset($bar) ? $bar : null);

// PHP 7+
$test = $foo ?? $bar ?? null;

/* ... */

// PHP 7+
someFunction($unknownVariable ?? 'default');

Оператор объединения с null
Тернарный оператор и оператор null-coalescing

Групповое объявление классов, констант и функций

Начиная с версии 7.0 появилась возможность группировать объявления импорта классов, функций и констант, находящиеся в одном пространстве имён, в одной строке с помощью оператора use. Это нововведение на уровне синтаксического сахара, которое может наделить объявление имён каких-то компонентов определённой логикой.

<?php

// PHP 5.6
use 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.6
use const app\namespace\ru_RU;
use const app\namespace\en_US;
use function app\namespace\firstFunc;
use function app\namespace\secondFunc;

// PHP 7+
use const app\namespace\{ru_RU, en_US};
use function app\namespace\{firstFunc, secondFunc};

Групповые декларации use

Декларация типов и возвращаемых значений

Это самое мощное нововведение для ООП. Теперь при объявлении метода для каждой переменной можно указать свой тип, а также тип данных, который вернёт этот метод. В PHP 7.0 доступны следующие типы: array, callable, bool, float, int, string, имя класса или интерфейса. В версии 7.1 добавили ещё void и iterable. Тип void можно использовать только в возвращаемых значениях функций, которые ничего не возвращают. Псевдо-тип iterable используется в качестве значения массива или объекта, реализующего интерфейс Traversable.

<?php

class Foo
{
    public static function bar(int $a, float $b, string $c) : int
    {
        return $a + $b + $c;
    }
}

// вернёт int(6)
var_dump( Foo::bar('1', 2, 3.0) );

В примере выше отработала принуждающая декларация типов, т. е. PHP динамически привёл значения к нужному типу, когда в переменную с объявленным типом int мы передали string, например. Вместо того, чтобы сгенерировать ошибку, все значения переменных и результат функции был преобразован к указанному типу на лету. Это поведение установлено по умолчанию.

Строгую типизацию можно включить, если в начале скрипта прописать declare(strict_types=1). Все значения должны полностью соответствовать указанным типам. В противном случае будет сгенерирована ошибка TypeError. Строгая типизация не затрагивает весь остальной код и её нужно прописывать для каждого скрипта отдельно.

<?php

// включили строгую типизацию
declare(strict_types=1);

class Foo
{
    public static function bar(int $a, int $b) : int
    {
        return $a + $b;
    }
}

// вернёт ошибку TypeError, т. к.
// первый аргумент является string,
// а должен int
var_dump( Foo::bar('1', 2) );

// вернёт int(3)
var_dump( Foo::bar(1, 2) );

В версии 7.1 появилась возможность обнулить возвращаемые типы. Это расширяет список возвращаеммых типов до null. Теперь функция может вернуть какой-то явно указанный тип или null. Достигается такое поведение путём добавления префикса в виде знака вопроса к указанному типу:

<?php

declare(strict_types=1);

class Foo
{
    public static function bar(?string $name) : ?string
    {
        return $name;
    }
}

// string(13) "Hello, world!"
var_dump( Foo::bar('Hello, world!') );

// NULL
var_dump( Foo::bar(null) );

// string(0) ""
var_dump( Foo::bar('' ?? null) );

// NULL
var_dump( Foo::bar($unknownVariable ?? null) );

// вернёт ошибку TypeError
var_dump( Foo::bar(1) );

// вернёт ошибку ArgumentCountError
var_dump( Foo::bar() );

Декларация типов и возвращаемых значений выводит PHP на новый уровень и эти нововведения мне нравятся больше всего, т. к. со стороны разработчика повышается читабельность кода; чёткая согласованность входных и выходных данных; программист может быстрее понять, чего ожидать от функции; легче документировать код; появляется возможность изменять типы данных на лету. Это основополагающая функциональность PHP7, которой не знать уже сейчас недопустимо.

Декларация скалярных типов
Декларация возвращаемых значений
Обнуляемые типы
Ничего не возвращающие функции
Псевдо-тип iterable

Обработка ошибок и исключений

В 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). Эти подклассы ошибок не могут быть брошены самостоятельно, а только лишь словлены. Иерархию всех исключений можно представить в виде дерева:

┌Throwable
├──Error
│  ├──ArithmeticError
│  ├──AssertionError
│  ├──DivisionByZeroError
│  ├──ParseError
│  ├──TypeError
│  └──ArgumentCountError
└──Exception
   ├──ErrorException
   ├──LogicException
   │  ├──BadFunctionCallException
   │  │  └──BadMethodCallException
   │  ├──DomainException
   │  ├──InvalidArgumentException
   │  ├──LengthException
   │  └──OutOfRangeException
   └──RuntimeException
      ├──OutOfBoundsException
      ├──OverflowException
      ├──RangeException
      ├──UnderflowException
      └──UnexpectedValueException

Начиная с версии 7.1 появилась возможность в блоке catch обрабатывать сразу несколько исключений, перечисляя их через символ вертикальной черты. Может быть полезно для обработки одинаковых по логике ошибок или пользовательских исключений.

<?php

try {
    /*
     * код программы
     */
} catch (
    ParseError         |
    TypeError          |
    ArgumentCountError $e
) {
    /*
     * ловим три вида ошибок PHP в одном блоке
     */
} catch (AppException | UserException $e) {
    /*
     * ловим пользовательские исключения
     */
} catch (Exception $e) {
    /*
     * ловим остальные исключения
     */
} catch (Throwable $t) {
    /*
     * ловим и логируем все ошибки и исключения
     */
}

На место фатальных ошибок пришли исключения, которые могут быть обработаны и позволяют корректно завершить программу в случае каких-то косяков. Это добавляет гибкости PHP-приложению.

Предопределённые исключения
Изменения в обработке ошибок и исключений
Обработка нескольких исключений в одном блоке catch

Остальные нововведения одним списком

Нововведения в PHP7+ это лишь начало большого перехода на новый уровень языка. С каждым релизом приложения будут становиться быстрее и безопаснее. На версии 7.0 и 7.1 нужно отреагировать уже сегодня и будь в курсе новых тенденций PHP, дабы не выпадать из сферы. Ниже привожу одним списком остальные нововведения:

7.0 Оператор spaceship (космический корабль)
7.0 Задание констант массивов с помощью define()
7.0 Появились анонимные классы
7.0 Синтаксис кодирования Unicode
7.0 Добавлен метод в замыкания Closure::call()
7.0 unserialize() с фильтрацией
7.0 Новый класс IntlChar
7.0 Улучшена старая функция assert()
7.0 Выражение return в генераторах
7.0 Делегация генератора с помощью конструкции yield from
7.0 Новая функция intdiv() для целочисленного деления
7.0 session_start() принимает массив опций
7.0 Новая функция preg_replace_callback_array()
7.0 Новые функции random_bytes() и random_int()
7.0 list() может раскрывать объекты реализующие 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 5.6.x к PHP 7.0.x
Миграция из PHP 7.0.x к PHP 7.1.x
Сравнение скорости для различных фреймворков в PHP7
Инфографика: Turbocharging the Web with PHP7
Введение в PHP 7: что добавлено, что убрано
PHP 7.1: обзор новых возможностей
Return types и Scalar type в PHP7
Throwable исключения и ошибки в PHP7

Закладки разработчика сайтов — выпуск #3

С начала этого года активно писал код для сервиса, у которого вся бизнес-логика завязана на работу с изображениями: загрузкахранениеобработкаотдача. Конечно же в браузере накопилось много закладок на эту тему. Самое вкусное публикую в этом выпуске. Здесь вы найдёте различные библиотеки на PHP и несколько крутых веб-сервисов. Для меня же наибольшую ценность представляет сам исходный код всего этого добра, потому как в них я нашёл много чего интересного для себя. Если ниже ничего не отображается, тогда воспользуйтесь запасной ссылкой.

Ещё ссылки по теме:

Cloudinary — облачное хранилище изображений
Uploadcare — конкурент Cloudinary от наших разработчиков
Imgix — набор инструментов для работы с картинками по URL
S-shot — сервис для создания скриншотов сайтов

Подписывайтесь на мой Твиттер, если хотите получать интересные и полезные ссылки по программированию и вёрстке сайтов до того, как они попадут в дайджест на блоге.

Полное руководство по загрузке изображений на PHP

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

§1. Общие принципы
§2. Правила безопасности
§3. Конфигурация php.ini
§4. Загрузка картинки из формы
§5. Загрузка изображения по ссылке
§6. Настройка выбора нескольких файлов

§1. Общие принципы

Всю последовательность загрузки изображения на сервер можно отобразить следующим образом: настройка php.iniполучение файлапроверка безопасностивалидация данныхсохранение на диск. Процесс загрузки картинки с компьютера пользователя или по URL ничем не отличаются, за исключением способа получения изображения и его сохранения. Общая схема загрузки картинки на сервер выглядит следующим образом:

Для валидации картинки по URL мы будем использовать функцию getimagesizefromstring(), т. к. cURL скачает её в переменную для дальнейших манипуляций.

Поскольку мы загружаем изображения на сервер, то хорошо было бы проверять их определённые параметры: ширину, высоту, тип картинки, размер файла в байтах. Это зависит от логики вашего приложения, но для наглядности в этом руководстве мы проверим все вышеописанные параметры.

§2. Правила безопасности

Безопасность загрузки изображений сводится к недопущению попадания на сервер чужеродного кода и его выполнения. На практике загрузка картинок наиболее уязвимое место в PHP-приложениях: попадание shell-скриптов, запись вредоносного кода в бинарные файлы, подмена EXIF-данных. Для того, чтобы избежать большинства методов взлома нужно придерживаться следующих правил:

а не доверять данным из $_FILES;
б не проверять MIME-тип картинки из функции getimagesize();
в загружаемому файлу генерировать новое имя и расширение;
г запретить выполнение PHP-скриптов в папке с картинками;
д не вставлять пользовательские данные через require и include;
е для $_FILES использовать is_uploaded_file() и move_uploaded_file().
Если есть чем дополнить «Правила безопасности», тогда оставляйте свои замечания или ссылки на статьи по безопасности в комментариях к этому руководству, а я опубликую их в этом параграфе.

Безопасная загрузка изображений на сервер
PHP check if file is an image
How to Securely Allow Users to Upload Files
Безопасная загрузка файлов на сервер
Надёжная защита веб-сайта от большинства видов атак
Ваш сайт тоже позволяет заливать всё подряд?
PHP image upload security check list
Unrestricted File Upload
Опасный getimagesize() или Zip Bomb для PHP
Почему надо пользоваться стандартными функциями при загрузке файлов
Руководство по написанию защищённых PHP-приложений в 2018-м

§3. Конфигурация php.ini

PHP позволяет внести определённые конфигурационные значения в процесс загрузки любых файлов. Для этого необходимо в файле php.ini найти блоки «Resource Limits», «Data Handling» и «File Uploads», а затем отредактировать, по необходимости, следующие значения:

[Resource Limits]
; Максимальное время выполнения скрипта в секундах
max_execution_time = 60

; Максимальное потребление памяти одним скриптом
memory_limit = 64M

[Data Handling]
; Максимально допустимый размер данных отправляемых методом POST
post_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" (в данном случае данные не кодируются и это значение применяется только для отправки бинарных файлов). С формой ниже мы будем работать дальше:

<form action="file-handler.php" method="post" enctype="multipart/form-data">
  <input type="file" name="upload">
  <button>Загрузить</button>
</form>

Для поля выбора файла мы используем имя 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 элементов:

Array
(
    [0]        => 1280                      // ширина
    [1]        => 768                       // высота
    [2]        => 2                         // тип
    [3]        => width="1280" height="768" // аттрибуты для HTML
    [bits]     => 8                         // глубина цвета
    [channels] => 3                         // цветовая модель
    [mime]     => image/jpeg                // MIME-тип
)

Для дальнейшей валидации изображения и работы над ним нам необходиом знать только 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);

// Переместим картинку с новым именем и расширением в папку /pics
if (!move_uploaded_file($filePath, __DIR__ . '/pics/' . $name . $format)) {
    die('При записи изображения на диск произошла ошибка.');
}

На этом загрузка изображения завершена. Для более удобной загрузки файлов можете использовать класс UploadedFile из пакета Symfony HttpFoundation, который является обёрткой для $_FILES и также сохраняет файл через move_uploaded_file().

§5. Загрузка изображения по ссылке

Для загрузки изображения по ссылке нам понадобиться библиотека 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);

// Укажем настройки для cURL
curl_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);   // Запишем код последней ошибки

// Завершим сеанс cURL
curl_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);

// Закроем ресурс FileInfo
finfo_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);

// Сохраним картинку с новым именем и расширением в папку /pics
if (!file_put_contents(__DIR__ . '/pics/' . $name . $format, $raw)) {
    die('При сохранении изображения на диск произошла ошибка.');
}

Для сохранения изображения на диск можно воспользоваться file_put_contents(), которая запишет контент в файл. Новое имя файла мы создадим через функцию md5(), а расширение сделаем из image_type_to_extension(). Теперь мы можем загружать любые картинки по ссылке.

§6. Настройка выбора нескольких файлов

В этом параграфе разберём способы загрузки нескольких изображений за один раз с локальной машины пользователя и по удалённым ссылкам. Для отправки ссылок мы задействуем $_POST и передадим ей все данные с помощью тега textarea. Для загрузки файлов из формы мы продолжим дальше работать с $_FILES. Наша новая HTML-форма будет немного отличаться от старой.

<form action="handler.php" method="post" enctype="multipart/form-data">
  <textarea name="upload"></textarea>
  <input type="file" name="upload[]" multiple>
  <button>Загрузить</button>
</form>

В конец имени поля выбора файла name="upload[]" добавились фигурные скобки и аттрибут multiple, который разрешает браузеру выбрать несколько файлов. Все файлы снова загрузятся во временную папку, если не будет никаких ошибок в php.ini. Перехватить их можно в $_FILES, но на этот раз суперглобальная переменная будет иметь неудобную структуру для обработки данных в массиве. Решается эта задача небольшими манипуляциями с массивом:

<?php

// Изменим структуру $_FILES
foreach($_FILES['upload'] as $key => $value) {
    foreach($value as $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 ($data as $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.

Дополнительное чтиво:

Как загрузить картинку по ссылке
Загрузка и хранение фотографий в Web приложениях
Отдаем файлы эффективно с помощью PHP
Хранение файлов
Теория хранения файлов на сервере
Можно ли полагаться на HTTP-заголовки при загрузке файла?
Загрузка файлов (картинок) на сервер через AJAX и jQuery
Как прочитать большой файл средствами PHP (не грохнув при этом сервак)
Как сделать Drag-and-Drop загрузчик файлов на чистом JavaScript
Хранение большого количества файлов

Ctrl + ↓ Ранее