среда, 26 июня 2013 г.

Curl, многомерные массивы и передача файлов

Задача: из кода на 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.