вторник, 26 августа 2014 г.

Ошибка при запуске screen

Узелок на память.

Последнее время очень часто сталкиваюсь с этой проблемой. При попытке запустить команду screen в консоли linux выскакивает сообщение об ошибке: Cannot open your terminal '/dev/pts/X' - please check. Важно! Перед выполнением screen производилось переключение между пользователями при помощи sudo su - user2.

Исправляется предельно просто. Выполняем в консоли:

script /dev/null

После этой команды screen запускается как нужно.

Пройдя по ссылке, можно прочитать объяснение, почему это работает.

воскресенье, 23 марта 2014 г.

Коллега, будь аккуратен

Передача произвольного числа параметров в функцию

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

Вот, например, функция, которая все передаваемые ей параметры выводит в столбик на экран.

function printVars() {
    $argsCount = func_num_args();
    $argsArray = func_get_args();
    for ($i = 0; $i < $argsCount; $i++) {
        echo $argsArray[$i] . "\n";
    }
} 

printVars('value 1', 'value 2', 'value 3');

Результат:

   value 1
   value 2
   value 3

Всю специальную логику обеспечивают два обращения к стандартной библиотеке: func_num_args для определения количества переданных параметров и func_get_args для доступа к этим параметрам.

Всё в этом коде хорошо, пока параметры мы передаём явно, но давайте представим такую, достаточно стандартную, ситуацию. Мы получили из базы данных какую-то запись, преобразовали её в одномерный массив и хотим передать элементы массива в функцию printVars. Сколько будет элементов массива, мы заранее не знаем. Вопрос на засыпку - как вызвать нашу функцию?

$valuesArray = array('value 1', 'value 2', 'value 3');
printVars(/* ??? */);

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

$quotesValuesArray = array_map(
    function ($item) { return "'$item'"; }, 
    $valuesArray);
eval("printVars(" . implode(', ', $quotesValuesArray) . ");");

Вариант получается неудобный и может стать источником непредсказуемых трудноуловимых ошибок. Этим, в принципе, страдает любой код, вызываемый через eval. Ужас, ужас!

Интереснее выглядит использование функции call_user_func_array. Она умеет вызывать некую пользовательскую функцию и передавать ей в качестве параметров значения из массива.

call_user_func_array("printVars", $valuesArray);

Код выглядит не идеально, но явно нагляднее, чем первый вариант. Да и мест для потенциальных ошибок стало меньше.

Если мы написали не просто функцию с переменным числом параметров, а метод класса, то вызов call_user_func_array меняется совсем незначительно.

class Example {
    public function printVars() {
        // ...
    }
}
// ...
$example = new Example();
call_user_func_array(array($example, "printVars"), $valuesArray);

Проведём ещё один эксперимент. Изменим функцию printVars, чтобы она изменяла и возвращала передаваемые ей параметры.

function printVars() {
    $argsArray = func_get_args();

    if (is_array($argsArray)) {
        foreach ($argsArray as $key => $arg) {
            $argsArray[$key] = $arg . '!';
        }
    }
}

$value1 = 'value 1';
$value2 = 'value 2';
$value3 = 'value 3';
$valuesArray = array(&$value1, &$value2, &$value3);

call_user_func_array("printVars", $valuesArray);

var_dump($valuesArray);

Получаем:

array(3) {
[0]=>
&string(7) "value 1"
[1]=>
&string(7) "value 2"
[2]=>
&string(7) "value 3"
}

Хм. Но это же совсем не то, что мы хотели получить! Параметры $value1, $value2 и $value3 не изменились. Оказывается, с помощью func_get_args нельзя получить ссылки на исходные параметры функции. Решается эта проблема с помощью злого хака.

function printVars() {
    $trace = debug_backtrace();
    $argsArray = $trace[1]['args'][1];

    if (is_array($argsArray)) {
        foreach ($argsArray as $key => $arg) {
            $argsArray[$key] = $arg . '!';
        }
    }

}

$value1 = 'value 1';
$value2 = 'value 2';
$value3 = 'value 3';
$valuesArray = array(&$value1, &$value2, &$value3);

call_user_func_array("printVars", $valuesArray);

var_dump($valuesArray);

Вот теперь мы добились нужного результата.

array(3) {
[0]=>
&string(8) "value 1!"
[1]=>
&string(8) "value 2!"
[2]=>
&string(8) "value 3!"
}

Но и это ещё не все проблемы. call_user_func_array возвращает "the function result, or false on error". А если наша функция возвращает false в качестве результата своей работы? Как нам определить, возникала ошибка или нет?

Однозначно определить не получится, но можно хотя бы чуть-чуть улучшить ситуацию при помощи предварительного вызова is_callable.

$handler = "printVars";
if (is_callable($handler)) { 
    $res = call_user_func_array( $handler , $valuesArray );
} else {
    throw new Exception();
}

Теперь мы хотя бы знаем, доступна ли нам вызываемая функция. Может быть, не стоит и пытаться её вызывать.

Третьим вариантом вызова функции с переменным числом параметров является механизм reflection. (Ума не приложу, как корректно перевести термин reflection на русский язык.)
function printVars() {
    $argsCount = func_num_args();
    $argsArray = func_get_args();
    for ($i = 0; $i < $argsCount; $i++) {
        echo $argsArray[$i] . "\n";
    }
}

$valuesArray = array('value 1', 'value 2', 'value 3');

$function_ref = new ReflectionFunction('printVars');
$res = $function_ref->invokeArgs($valuesArray);

Этот код имеет ту же проблему с передачей параметров по ссылке, что и вариант с call_user_func_array, но с возвращением "false" в качестве результата выполнения функции всё хорошо. invokeArgs всегда возвращает результат вызываемой функции, в случае же возникновения ошибки бросается исключение ReflectionException.

А что со скоростью выполнения? Усреднённые результаты двадцати прогонов по 10 000 вызовов одной и той же функции с переменным числом параметров:

  • eval - 0.113432610035 мкс
  • call_user_func_array - 0.0677324056625 мкс
  • reflection - 0.063702750206 мкс

Тесты проводились на PHP 5.3.10. Учитывая сомнительное качество тестового стенда, разностью в скорости работы call_user_func_array и reflection можно смело пренебречь, а вот eval и тут проявил себя хуже всех.

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

вторник, 13 августа 2013 г.

Тонкости именования

Когда только начинаешь писать код, то кажется, что самое важное в работе это новомодные технологии, шаблоны проектирования, наследование объектов и прочие полиморфизмы. Что стоит использовать какую-то магическую инверсию зависимости и код сразу станет близок к идеальному. На всякие мелочи, типа стандартов оформления, именования переменных, комментирования методов, отвлекаться не хочется, да и банально жалко терять драгоценное время, которое можно потратить на написание крутой иерархии классов или реализацию какого-нибудь красивого всплывающего окошка. К тому же окошком можно похвастаться перед друзьями, а логичным названием переменной хвастаться как-то нелепо. "Смотрите, смотрите, переменную, в которой хранится, количество заказов, я назвал ordersCount!"

С опытом приходит понимание, что технологии - вещь, бесспорно, необходимая, но есть базовые определяющие вещи, которым обязательно нужно уделять внимание в любое время и в любом языке программирования. И как-раз корректное именование переменных и методов - одна из тех задач, решив которую, можно абсолютно бесплатно упростить поддержку кода, а значит и глобально улучшить в долгосрочной перспективе качество разрабатываемого проекта.

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

1. При перечислении объектов в цикле текущий объект всегда называется "oneObject"

Вот пример кода, который я писал раньше:

foreach ($orders as $order) {
    var_dump($order);
}

Проблема в том, что имена переменных, отличающиеся только одной буквой, слишком похожи друг на друга. Из-за этого возникают трудности при чтении такого кода. Кроме того, это потенциальное место для внесения ошибок. Перепутать переменные order и orders ничего не стоит. Теперь я пишу так:

foreach ($orders as $oneOrder) {
    var_dump($oneOrder);
}

Различие между переменными $orders и $oneOrder стали настолько существенными, что нельзя представить ситуацию, когда кто-нибудь случайно их перепутает. Также хорошей альтернативой oneObject может быть использование префикса current. Например, для нашего примера, текущий заказ в цикле можно назвать - currentOrder.

Мой выбор в пользу "one" основывался на том, что в функциях, с которыми мне приходится работать, префикс "current" часто используется для переменных вне циклов. Поэтому для меня oneObject имеет более определённый смысл, чем currentObject.

Upd. Ещё один вариант именования переменной внутри цикла подсмотрел в книжке у Кента Бека. Он предлагает использовать просто $each для небольших циклов и $each<Object> для более крупных. Например, $eachRow или $eachOrder.

2. Функция получения списка объектов называется getObjects, функция получения одного объекта - detailObject

Если используем getObjects и getObject, то опять же имеем проблемы с похожестью имён. Нужно правило именования, которое не позволяет вызвать функцию по ошибке. getObjects и detailObject решают проблему. Неплохим выбором может быть и выбор пар getObject и getObjectList или getObject и listObjects. Отдать предпочтение можно любому из вариантов. Главное условие - чтобы принятый вариант последовательно использовался во всём коде.

3. Если тип является значимым свойством переменной, то он должен указываться в имени

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

$orderIds = array(1, 2, 3, 4, 5);
$order{???} = implode(',', $orderIds);
buildOrdersQuery($order{???});

Как назвать переменную - результат выполнения функции implode? Для данной переменной то, что она строкового типа, является значимым фактом, Именно для преобразования массива в строку она и была заведена. Нет ничего плохого, чтобы отразить этот факт в её имени,

$orderIds = array(1, 2, 3, 4, 5);
$orderIdsAsString = implode(',', $orderIds);
buildOrdersQuery($orderIdsAsString);

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

4. Булевские переменные называем с использованием префиксов не допускающих неоднозначного толкования хранящихся в них значений

Удобство следованию правилу "булевские переменные называем с использованием префикса is" доказано временем. Из названия переменной сразу понятно, что означает то или иное значение. Но кроме отличного слова "is" есть и другие слова, использование которых также не допускает неоднозначности в толковании значения. Например, has или allow. Не нужно ими пренебрегать

$clientHasDiscount = TRUE;
...
if ($user->allowAccessToCP) { ...

Нельзя предположить, что, если в переменной $clientHasDiscount хранится FALSE, то у клиента есть скидка. Скидка только в том случае, если $clientHasDiscount станет равно TRUE. Название переменной "самодокументировало" своё использование.

5. Если функция возвращает статус выполнения, то переменную, хранящую этот результата надо называть не $res, $result или $status, а $success или $done

Причины те же, что и в предыдущем правиле. Уже из название становится понятен смысл допустимых значений.

public function updateOrder(Order $order) {
    $success = FALSE;
    if ($options->allowOrderUpdate) {
        $success = Db::saveOrder($order);
    }
    return $success;
}

Абсолютно ясно, в каком случае функция отработала нормально, а в каком обновление не состоялось или прошло с ошибками.

Ну, и в качестве бонуса, дочитавшим до конца - немного хорошей музыки в тему заметки. Aрия Рейстлина из мюзикла "Последнее испытание". "Я дам тебе имя."

среда, 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 и прибудет с вами сила!