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

воскресенье, 24 февраля 2013 г.

Обязательные конструкторы

Объектно-ориентированное программирования в PHP такое... объектно-ориентированное...

Пусть есть базовый класс 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 называется конструктором, на самом деле - обычная функция инициализации. Но при этом абсолютно непонятно, что делать, чтобы не допускать связанных с этим ошибок. Остаётся быть внимательным, смириться и пользоваться тем, что есть.