суббота, 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)) {}

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

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

вторник, 9 октября 2018 г.

Автокомплит для поля с предустановленным значением

Недавно столкнулся со следующей проблемой. Имеется форма с автокомплитом, реализованным через плагин devbridge/jQuery-Autocomplete. При первой загрузке поле, к которому привязан автокомплит, не заполнено, пользователь начинает что-то вводить и получает подсказки. Отдельно уточню, что подсказки выводятся в результате ajax-запроса на сервер. Далее, при выборе конкретной подсказки происходит сабмит формы, и страница перезагружается. Тут все хорошо.

После перезагрузки страницы в поле с автокомплитом уже предустановлено поисковое значение из предыдущего шага.

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

Мое решение не претендует на правильность, возможно, есть какая-то опция в настройках, но я ее не нашел. Поэтому решил сделать следующее: перед активацией автокомплита фиксируем начальное значение поля. Далее используем опцию onSearchStart, которая определяет, можно ли начинать поиск или нет. В этой функции проверяем - если начальное значение поля пустое или текущее значение отличается от начального, то возвращаем true, разрешая поиск подсказок, иначе возвращаем false и запрещаем поиск. Получается вот такой сеттинг (html + javascript):

<div id="autocomplete_field_wrap">
    <input id="autocomplete_field" type="text" value="" />
</div>
<script src="./jquery.min.js"></script>
<script src="./jquery.autocomplete.js"></script>
<script>
$(document).ready(function () {
   var initialValue = $('#autocomplete_field').val();

   $('#autocomplete_field').autocomplete({
       appendTo: $('#autocomplete_field_wrap'),
       dataType: "json",
       minChars: 3,
       paramName: "q",
       serviceUrl: 'ajax/autocomplete.php',
       onSearchStart: function () {
           return initialValue === "" || initialValue !== $('#autocomplete_field').val();
       },
       onSelect: function () {
           console.log('value selected');
       }
   });
});
</script>


С поставленной задачей такой костылик вполне справляется.

суббота, 29 сентября 2018 г.

Прерывание работы компонента на классе

При работе с компонентом иногда приходится прерывать работу компонента еще на стадии проверки входных параметров $arParams. Например, если вам требуется ИД инфоблока, а его не указано, то нет смысла заводить всю шарманку с кешированием и прочим, так как получать в общем-то нечего.

В компонентах, построенных на процедурном подходе в файле component.php это решалось приблизительно так:

if (!CModule::IncludeModule('iblock')) {
    ShowError(GetMessage("IBLOCK_MODULE_NOT_INSTALLED"));
    return;
}


В этом случае на странице выводилось красным текстом какое-либо сообщение, и страница продолжала свою работу.
Теперь давайте рассмотрим как реализовать сходное поведение в компоненте на классах (с файлом class.php вместо component.php).