Curl, многомерные массивы и передача файлов
Mon, Dec 14, 2015Задача: из кода на PHP передать веб-службе файл с помощью curl.
Решение стандартное и достаточно простое, но, как обычно, при программировании на PHP есть нюансы.
Инициализируем библиотеку curl, формируем данные для POST-запроса, одним из параметров устанавливаем путь к передаваемому файлу, который обязательно начинаем со значка “собаки”.
$requestVars = array(
'id' => 1234,
'name' => 'log',
'logfile' => '@/tmp/test.log');
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'test.web.service.net');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $requestVars);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$res = curl_exec($ch);
curl_close($ch);
Смотрим на стороне веб-службы переменные $_POST и $_FILES.
$_POST: Array (
[id] => 1234
[name] => log
)
$_FILES: Array (
[logfile] => Array (
[name] => test.log
[type] => application/octet-stream
[tmp_name] => /tmp/phpfdWZF6
[error] => 0
[size] => 11
)
)
Всё прошло отлично. Curl самостоятельно принял решение об использовании при передаче запроса алгоритма multipart/form-data и передал файл веб-службе. На её стороне файл сохранён с именем /tmp/phpfdWZF6.
Теперь попробуем изменить параметры запроса, добавив туда вложенные массивы.
$requestVars = array(
'id' => array(1, 2, 3, 4),
'name' => 'log',
'logfile' => '@/tmp/test.log');
Файл веб-служба загрузила, а вот в переменной $_POST теперь некорректные данные.
$_POST: Array (
[id] => Array
[name] => log
)
Проблема в том, что библиотека curl не умеет обрабатывать вложенные массивы, установленные в CURLOPT_POSTFIELDS. Она работает только с одноуровневыми массивами или строкам. Поэтому самое первое и очевидное решение - превратить массив в строку с помощью http_build_query. Пробуем.
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($requestVars, '', '&'));
Параметры в $_POST передались как нужно, но теперь вообще не передался файл.
$_POST: Array (
[id] => Array (
[0] => 1
[1] => 2
[2] => 3
[3] => 4
)
[name] => log
[logfile] => @/tmp/test.log
)
$_FILES: Array ( )
Т.к. параметры запроса были переданы строкой, то библиотека curl проявила интеллект и начала использовать алгоритм передачи данных application/x-www-form-urlencoded, который не имеет даже теоретической возможности передачи файла. Продолжаем борьбу. Устанавливаем принудительно в заголовках запроса нужный нам тип контента.
$headers[] = "Content-type: multipart/form-data";
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
После этого у нас вообще перестаёт работать отправка запроса. Ни параметры запроса, ни файл веб-служба не получает. А всё потому, что начиная с PHP версии 5.2.0 при передаче файлов с префиксом “@” значение в CURLOPT_POSTFIELDS обязательно должно быть массивом. С одной стороны, чтобы передать многоуровневый массив, нам нужна строка. С другой стороны, для передачи файла нужен массив.
Решение, как обычно, посередине. Строим одноуровневый массив с такими ключами, какими они были бы при формировании строки. Т.е. приводим наши параметры к такому виду.
$requestVars = array(
'id[0]' => 1,
'id[1]' => 2,
'id[3]' => 3,
'id[4]' => 4,
'name' => 'log',
'logfile' => '@/tmp/test.log');
curl_setopt($ch, CURLOPT_POSTFIELDS, $requestVars);
И вот теперь мы, наконец-то, получили то, что хотели.
$_POST: Array (
[id] => Array (
[0] => 1
[1] => 2
[3] => 3
[4] => 4
)
[name] => log
)
$_FILES: Array (
[logfile] => Array (
[name] => test.log
[type] => application/octet-stream
[tmp_name] => /tmp/phpfdWZF6
[error] => 0
[size] => 11
)
)
Осталось написать универсальное решение, которое бы преобразовывало любые многоуровневые массивы в одноуровневые. Тут на помощь приходит простенькая рекурсия.
function convertToStringArray(
$inputKey, $inputArray, &$resultArray) {
foreach ($inputArray as $key => $value) {
$tmpKey = (bool)$inputKey ? $inputKey."[$key]" : $key;
if (is_array($value)) {
convertToStringArray($tmpKey, $value, $resultArray);
} else {
$resultArray[$tmpKey] = $value;
}
}
}
Тестируем последний раз.
$requestVars = array(
'id' => array(1, 2, 3, 4),
'name' => 'log',
'logfile' => '@/tmp/test.log');
$resultArray = array();
convertToStringArray('', $requestVars, $resultArray);
$requestVars = $resultArray;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'test.web.service.net');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $requestVars);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$res = curl_exec($ch);
curl_close($ch);
Убеждаемся, что всё работает, и отправляемся на поиски новых открытий в прекрасном и удивительном мире программирования на PHP.