воскресенье, 20 октября 2019 г.

Неявные преобразования данных в PHP

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

Допустим, у вас есть такой простенький код:

$a = ['1' => 'v1', '1-2' => 'v2', '1-3' => 'v3'];
$search = '1-2';
foreach ($a as $key => $value) {
    if ($key == $search) {
        echo 'Key found: ' . $key . PHP_EOL;
    }
}


Видим вывод:

Key found: 1
Key found: 1-2


Возникает закономерный вопрос - почему? Мы же ожидали, что выведется только ключ 1-2. Откуда же в выводе взялся ключ 1?

Давайте копнем чуть глубже и модифицируем код проверки:

if ($key == $search) {
    var_dump($key, $search);
    echo PHP_EOL;
}


Видим вывод:

int(1)
string(3) "1-2"

string(3) "1-2"
string(3) "1-2"


Что мы видим? Что ключ первого элемента массива вместо типа строка (string) стал типом целое число (integer).

Это стандартное поведение ключей типа строка, отмеченное в официальном руководстве. Получаем, что на первой итерации цикла мы сравниваем целое число 1 со строкой 1-2. Немного неожиданно.

Казалось бы - строка 1-2 уж никак не может быть равна целому числу 1. Однако, правила сравнения разных типов (также отраженные в официальном руководстве, таблица Сравнение различных типов) сообщают, что при сравнении числа и строки, строка приводится к числу. И по правилам приведения к числу (опять же описанным к официальном руководстве) мы получаем, что строка 1-2 приводится к числу 1. А уж 1 точно равно 1.

Что можно сказать в заключение:

P.S. Интересно, предложат и заапрувят ли когда-нибудь rfc, который сделает оба сравнения (=== и ==) строгими?

вторник, 24 сентября 2019 г.

Список аргументов консольного скрипта

Всем привет. Сегодня будем разбираться, как получить список аргументов php-скрипта, запущенного из консоли.

Обычно запуск скрипта выглядит так:

> php script.php run 20 zzz
// или если у скрипта есть право на исполнение
> ./script.php run 20 zzz


Как же получить переданные в командной строке аргументы run, 20, zzz?

Для этого в php есть две зарезервированные переменные:
  • $argv - массив, содержит список аргументов. При этом учтите, что имя исполняемого скрипта также является аргументом командной строки и присутствует в списке аргументов. Поэтому, чтобы обратиться к значению run из нашего примера требуется использовать $argv[1], а не 0, так как $argv[0] - это script.php, офссылка.
  • $argc - число, содержит количество переданных аргументов, также учитывается имя исполняемого скрипта. Для нашего примера $argc равно 4, офссылка.

Как и любые другие глобальные переменные в скрипте, $argv и $argc не защищены от перезаписи. Так что если вы где-то в вашем скрипте напишете $argv = 42; то все ваши входные аргументы будут потеряны. Также, эти переменные не являются суперглобальными, то есть использовать их в функциях без явного указания global (фу) или передачи как аргумент функции - не получится.

Однако, выведя на экран содержимое суперглобальной переменной $_SERVER можно заметить, что данные из $argv и $argc дублируются в аналогичных ключах массива $_SERVER - argv и argc.

В случае если вы хотите передавать именованные аргументы, например:

> php script.php --action=run --time=20 --option=zzz

то php их никак не парсит и просто выдает в $argv массив вида:

Array
(
    [0] => script.php
    [1] => --action=run
    [2] => --time=20
    [3] => --option=zzz
)


Для парсинга таких входных данных существует функция getopt, но ее рассмотрение - это повод для отдельного поста.

суббота, 6 июля 2019 г.

Перенос данных нескольких полей в единое поле hstore

Сегодня опишу вам как решить следующую сверхспецифическую задачу: у нас в БД PostgreSQL есть таблица некоторой структуры:

id | name | attr_1 |  attr_2 |  attr_3 |
---------------------------------------|
 1 | NAME |     v1 |      v2 |      v3 |


Вследствие каких-то причин в проекте решено соединить данные полей attr_1, attr_2, attr_3 в одно поле attrs типа hstore.

Таким образом, новая таблица и запись в ней выглядит так:

id | name |                                        attrs |
---------------------------------------------------------|
 1 | NAME |     attr_1 => v1, attr_2 => v2, attr_3 => v3 |


Естественно, будем все максимально автоматизировать. Для работы нам пригодятся некоторые функции для работы с hstore и немного php, просто чтобы сформировать общий текст запроса. Общий текст запроса выглядит вот так:

UPDATE tableName
SET attrs =
    hstore(
        string_to_array(
            rtrim(
                (CASE WHEN (attr_1 IS NOT NULL) THEN ('attr_1' || '~~~' || attr_1 || '~~~') ELSE '' END)
                ||
                (CASE WHEN (attr_2 IS NOT NULL) THEN ('attr_2' || '~~~' || attr_2 || '~~~') ELSE '' END)
                ||
                (CASE WHEN (attr_3 IS NOT NULL) THEN ('attr_3' || '~~~' || attr_3 || '~~~') ELSE '' END),
                '~'
            ),
            '~~~'
        )
    )


Итак, что же здесь происходит? Начнем с внутренней части.

1. Для значения каждого из полей attr_1, attr_2, attr_3 мы создаем строку вида НазваниеПоля~~~ЗначениеПоля~~~ или просто пустую строку, если значение поля NULL. Все эти строки объединяем в одну результрующую.

2. Далее нам требуется избавиться от ~ в конце объединенной строки. В этом нам помогает rtrim.

3. Потом из объединенной строки мы создаем массив, разбивая строку по разделителю ~~~.

4. И, наконец, полученный массив передаем в метод hstore. Готово.

Отдельно замечу, что разделителем выбран ~~~ потому, что встретить его в значениях полей attr_1, attr_2, attr_3 невозможно. Если в ваших данных может встречаться такой набор символов - используйте другой разделитель из более "странных" символов.

С использованием php можно создать такой скрипт генерации и выполнения запроса:

$fields = [
    'attr_1',
    'attr_2',
    'attr_3',
    // еще поля
];
$glue = '~~~';
$selectPattern = "(CASE WHEN (%s IS NOT NULL) THEN ('%s' || '$glue' || %s || '$glue') ELSE '' END)";
$select = [];
foreach ($fields as $field) {
    $select[] = vsprintf($selectPattern, array_fill(0, 3, $field);
}
$select = implode(' || ', $select);

$this->runSql("
    UPDATE {$table}
    SET attrs = hstore(string_to_array(rtrim(({$select}), '~'), '{$glue}'))
");


По ссылке - улучшенный гист с кодом, обрабатывающим даже поле типа datetime.

среда, 27 марта 2019 г.

Трансформация DateTime в DateInterval

Недавно в вопросе на stackoverflow.com автор пытался добавить к объекту \DateTime некоторый временной интервал. Интервал, однако, также являлся объектом \DateTime. Так как метод add ожидает на вход объект \DateInterval, то ничего не работало. Возможно, есть другие решения этой проблемы, но мне пришло в голову такое:

$dateAppointment = (new \DateTime());
$dtDuration = (new \DateTime())->setTime(1, 15, 0);
// Для примера получаем интервал только с учетом часов, минут и секунд
$duration = $dtDuration->format('\P\TH\Hi\Ms\S');
$dateAppointment->add(new \DateInterval($duration));

воскресенье, 10 февраля 2019 г.

Строковые представления типов данных, часть 1

Всем привет. Сегодня будем разбираться в том, как php преобразует различные типы данных к строковому виду.

Приведение к строке используется в нескольких случаях. Главный из них - это вывод с помощью конструкции echo. Обращаю внимание, что функции var_dump и print_r также выводят данные, однако, руководствуются своими алгоритмами для вывода.

Второй случай, где работает приведение к строке это когда вы конкатенируете или подставляете в строку какую-либо переменную, например:

$strConcat = 'Эту строку я конкатенирую с переменной ' . $var;
$strEval = "В эту строку я подставляю переменную $var";


Начнем издалека. Для полноты картины я добавлю работу с переменными типа строка, но понятно что здесь нет никаких подводных камней)

$var = 'foo';
$strConcat = 'Эту строку я конкатенирую с переменной ' . $var;
$strEval = "В эту строку я подставляю переменную $var";
echo $var;
echo $strConcat;
echo $strEval;
// вывод:
"foo"
"Эту строку я конкатенирую с переменной foo"
"В эту строку я подставляю переменную foo"


Перейдем с булевым переменным. Булевая переменная принимает одно из двух значений - true или false. Каждое из них приводится к разному строковому представлению:

$var = false;
$strConcat = 'Эту строку я конкатенирую с переменной ' . $var;
$strEval = "В эту строку я подставляю переменную $var";
echo $var;
echo $strConcat;
echo $strEval;
// вывод:
"" // на самом деле здесь ничего не выводится, я просто отмечаю что это пустая строка
"Эту строку я конкатенирую с переменной "
"В эту строку я подставляю переменную "


$var = true;
$strConcat = 'Эту строку я конкатенирую с переменной ' . $var;
$strEval = "В эту строку я подставляю переменную $var";
echo $var;
echo $strConcat;
echo $strEval;
// вывод:
"1"
"Эту строку я конкатенирую с переменной 1"
"В эту строку я подставляю переменную 1"


Следующий, отдельный тип данных - null. Здесь все просто:

$var = null;
$strConcat = 'Эту строку я конкатенирую с переменной ' . $var;
$strEval = "В эту строку я подставляю переменную $var";
echo $var;
echo $strConcat;
echo $strEval;
// вывод:
"" // на самом деле здесь ничего не выводится, я просто отмечаю что это пустая строка
"Эту строку я конкатенирую с переменной "
"В эту строку я подставляю переменную "


Таким образом, если вы выводите переменную, а получаете пустую строку, то не факт, что в переменной хранится пустая строка. В переменной с таким же успехом может храниться false или она вообще может быть не определена (null). Поэтому следует использовать var_dump, чтобы увидеть реальное значение переменной.

В следующей части рассмотрим преобразование к строке числовых типов данных - int и float.

пятница, 11 января 2019 г.

Локальный .gitignore

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

И такой файл есть. Это файл .git/info/exclude. В нем, в формате аналогичном .gitignore, можно указать все свои исключения и, тем самым, почистить вывод git status от лишних файлов. А так как папка .git не связана с удаленным репозиторием, то эта информация никуда не уедет. Единственно, не забудьте этот файл, если вдруг будете переезжать, например, на другой компьютер или разворачивать репозиторий заново где-нибудь по другому пути.

Больше про игноры в оф-доке.

пятница, 23 ноября 2018 г.

Проверка isset() && !empty()

Очень часто в коде разработчиков встречается конструкция вида:

if (isset($var) && !empty($var)) {}

Аргументация людей, пишущих такой код, заключается в следующем:
empty выведет warning если переменная $var не была определена ранее.
Теперь откроем официальный мануал по empty и прочтем:
empty() does not generate a warning if the variable does not exist.
В русском варианте:
empty() не генерирует предупреждение, если переменная не существует.
Что это значит? Это значит, что все коды вида:

if (isset($var) && !empty($var)) {}

можно взять и заменить на:

if (!empty($var)) {}

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

На этом тему можно закрывать.