вторник, 17 декабря 2019 г.

Сборка мусора в php

Недавно встретился хороший пост для начинающих о сборке мусора (он же garbage collection) в php. Далее я попытаюсь перевести этот пост на русский и добавить немного собственных данных. Ссылка на оригинальный пост - в конце.

Начнем с того, что так как php - язык интерпретируемый, то вам не нужно заморачиваться управлением памятью - выделением памяти, и что более важно - очисткой памяти. Этим в php занимается специальный механизм, называемый сборкой мусора (или garbage collection, или же gc).

Сборка мусора работает тремя способами:
  1. При уходе переменной из области видимости
  2. При подсчете ссылок
  3. При сборе циклических ссылок

четверг, 12 декабря 2019 г.

Or-pattern в glob

Короткий пост о том, как в аргументе функции glob использовать шаблон или.

Предположим, каталог содержит следующие файлы:

- file.txt
- picture
- picture.gif
- picture.jpg
- picture.png
- picture1.jpg


И требуется получить файлы имеющие в названии только "picture" ("picture1" не подходит) и с расширениями jpg, png или вообще без расширения.

Естественно, простейшим решением можно считать объединение результатов трех вызовов glob:

print_r(array_merge(
    glob('./picture\.jpg'),
    glob('./picture\.png'),
    glob('./picture')
));


Но давайте попробуем ограничиться одним вызовом. В этом нам поможет изучение странички мануала функции glob, а точнее - списка параметров. В нем нас интересует второй аргумент flags и его значение GLOB_BRACE:

print_r(
    glob('./picture{,\.jpg,\.png}', GLOB_BRACE)
);


Получаем тот же самый набор файлов, что и в первом примере (сортировка не в счет). На этом всё, читайте почаще мануалы и находите решения попроще. Кстати, в комментах можете поделиться еще более хитрыми шаблонами для glob, если у вас таковые есть.

среда, 20 ноября 2019 г.

Удаление всех таблиц в схеме данных postgresql

Иногда требуется удалить все таблицы в выбранной схеме в базе данных postgresql.

Вы можете сказать - можно удалить сразу всю схему (или еще убойней - всю базу). Но нет, при удалении схемы или базы удалится все остальное, что напрямую не зависит от таблиц. Поэтому просто удалим только таблицы. Для этого нам пригодится следующий запрос, результатом которого будет список запросов на удаление каждой таблицы:

SELECT 'drop table if exists "' || tablename || '" cascade;' as pg_tbl_drop
FROM pg_tables
WHERE schemaname='public';


Естественно, вместо schemaname='public' нужно подставить вашу конкретную схему.

Выполнив все полученные запросы, получаем схему без таблиц. Однако, может возникнуть ситуация, что в схеме остались последовательности (sequences). Получить все запросы на удаление последовательностей можно таким запросом:

SELECT 'drop sequence if exists "' || relname || '" cascade;' as pg_sec_drop
FROM pg_class
WHERE relkind = 'S';


Посмотреть все последовательности (на всякий случай) можно с помощью запроса:

SELECT c.relname
FROM pg_class c
WHERE c.relkind = 'S';


И как всегда - при удалении важных данных не забудьте про бэкап.

воскресенье, 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";


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

$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 не связана с удаленным репозиторием, то эта информация никуда не уедет. Единственно, не забудьте этот файл, если вдруг будете переезжать, например, на другой компьютер или разворачивать репозиторий заново где-нибудь по другому пути.

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