четверг, 4 июля 2013 г.
среда, 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.
воскресенье, 10 марта 2013 г.
Ссылки внутри цикла foreach
$list = array(1, 2, 3);
foreach ($list as &$item) {
$item++;
}
print_r($list);
Запускаем, получаем логичный вывод:Array
(
[0] => 2
[1] => 3
[2] => 4
)
А теперь лёгким движением руки этот код превращается... превращается... в конструкцию со странным неочевидным функционалом.
$list = array(1, 2, 3);
foreach ($list as &$item) {
$item++;
}
print_r($list);
foreach ($list as $item) {
;
}
print_r($list);
Мы всего лишь организовали второй цикл foreach по одному и тому массиву. И в этот раз print_r выдал совсем иные результаты.Array
(
[0] => 2
[1] => 3
[2] => 3
)
Что же произошло? Собственно, ничего такого, чтобы мы сами не просили сделать PHP. В первом цикле мы объявили ссылку &$item, которая после завершения работы цикла указывает на элемент массива $list[2]. Далее мы пробегаемся ещё раз по массиву, на каждом шаге присваивая переменной $item очередное значение. Т.к. в PHP область видимости переменных не ограничивается блоком составного оператора, то переменная $item во втором цикле - это та же самая переменная из первого цикла. Поэтому, одновременно с установкой значения переменной $item, это же значение присваивается и элементу $list[2].
Шаг 0: $item = $list[2] = $list[0] = 2
Шаг 1: $item = $list[2] = $list[1] = 3
Шаг 2: $item = $list[2] = $list[2] = 3
Никаких ошибок нет, но получить-то мы хотели несколько другой результат. Есть несколько вариантов, чтобы обезопасить себя от таких неожиданностей. Первый вариант - никогда не забываем принудительно чистить переменные. Вот этот код всегда работает так, как задумано.
$list = array(1, 2, 3);
foreach ($list as &$item) {
$item++;
}
print_r($list);
unset($item); // !!!
foreach ($list as $item) {
;
}
print_r($list);
Второй вариант - для изменения элементов массива не используем ссылки, используем конструкцию $key => $value. С ней также нет никаких проблем.
$list = array(1, 2, 3);
foreach ($list as $key => $item) {
$list[$key]++;
}
print_r($list);
foreach ($list as $key => $item) {
;
}
print_r($list);
Ну и самый правильный вариант, при котором мы не только избегаем описанной проблемы, но и многих других - максимально сокращаем область видимости переменных, выделяя логические части кода в отдельные функции. Этот вариант неплохо бы совмещать с одним из первых двух.
$list = array(1, 2, 3);
$incrementList = function ($list) {
foreach ($list as $key => $item) {
$list[$key]++;
}
return $list;
};
$list = $incrementList($list);
print_r($list);
$doList = function ($list) {
foreach ($list as $key => $item) {
;
}
return $list;
};
$list = $doList($list);
print_r($list);
Здесь $item первого цикла и $item второго цикла - разные переменные.Пишите на PHP и прибудет с вами сила!
воскресенье, 24 февраля 2013 г.
Обязательные конструкторы
Пусть есть базовый класс BaseClass. Создаём его потомка - класс ChildClass. Создаём конструктор и "на автомате" вызываем конструктор базового класса.
class BaseClass { }
class ChildClass extends BaseClass {
public function __construct() {
parent::__construct();
// какой-то код
}
}
$obj = new ChildClass();
Если в базовом классе конструктор не объявлен явно, то мы рискуем получить сюрприз в виде "PHP Fatal error: Cannot call constructor". Чтобы избавиться от ошибки, либо не вызываем конструктор предка, что в большинстве случаев плохая практика, либо пишем пустой конструктор в базовом классе.В коде ниже ошибка уже не появляется.
class BaseClass {
public function __construct() { }
}
class ChildClass extends BaseClass {
public function __construct() {
parent::__construct();
// какой-то код
}
}
$obj = new ChildClass();
Аналогичная ситуация с деструктором. Не хочешь проблем - пиши пустой метод. Мелкое неудобство, но не позволяет полностью скрыть реализацию базового класса. Всегда нужно смотреть в код и проверять наличие конструкторов/деструкторов. Отдельной интриги добавляют приватные конструкторы и деструкторы.Причина такого поведения понятна. То, что в PHP называется конструктором, на самом деле - обычная функция инициализации. Но при этом абсолютно непонятно, что делать, чтобы не допускать связанных с этим ошибок. Остаётся быть внимательным, смириться и пользоваться тем, что есть.
