вторник, 21 января 2025 г.

Веб-разработка на Rust, часть 1

Всем привет. Это первый пост из серии постов, в котором я попробую создать полноценное веб-приложение на Rust. Зачем? Потому что могу)
В этом посте я:

  1. собственно, установлю Rust
  2. создам и запущу hello-world приложение
  3. перейду к использованию Cargo

1 Итак, сначала установим Rust.
Для этого перейдём по адресу https://www.rust-lang.org/tools/install и воспользуемся рекомендуемым способом (Using rustup (Recommended)), а именно - в консоли запускаем:

> curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Через некоторое время видим надпись

Rust is installed now. Great!

Проверим установленную версию компилятора Rust:

> rustc -V > rustc 1.83.0 (90b35a623 2024-11-26)

Больше информации об установке (а также удалении) Rust можно узнать на уже указанной странице https://www.rust-lang.org/tools/install.

2 Теперь, по классике, напишем hello-world приложение.
Создадим папку для нашего проекта, например my-rust-project и перейдем в неё. Далее создадим файл main.rs, в котором и напишем код приложения:

> mkdir my-rust-project && cd ./my-rust-project > touch main.rs

Код приложения выглядит следующим образом:

fn main() { println!("Hello, World!"); }

Комплилируем и запускаем приложение:

> rustc main.rs > ./main

Видим вывод: Hello, World!

Приложение работает, можно переходить к следующему шагу! А больше подробностей о создании первого (и не только) приложения на Rust находится в официальной документации.

3 Перейдем к использованию Cargo
Запускать каждый раз rustc, а потом скомпилированный файл, конечно, можно, но для более продуктивной работы разработчики Rust создали Cargo - билд-менеджер и менеджер зависимостей в одном лице. Перепишем же наше hello-world приложение, используя Cargo.

Создаем новый проект:

> cargo new cargo-rust-project

Перейдя в папку cargo-rust-project, видим что Cargo создал нам некоторую структуру проекта, которой мы будем пользоваться в дальнейшем. В папке cargo-rust-project/src уже содержится файл main.rs с кодом hello-world приложения, так что даже и писать ничего не надо. Остается только скомпилировать и запустить, это делается единственной командой:

> cargo run

Видим вывод: Hello, World!

Итак, наше первое hello-world приложение на Rust работает, а значит время переходить к следующему этапу.

Больше информации о Cargo - в официальном руководстве.

воскресенье, 26 мая 2024 г.

Изменение автора коммита в git

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

Чтобы такая ситуация не повторялась, измените в репозитории файл .git/config и внесите туда информацию о нужном аккаунте в виде:

[user]
    email = developer@domain.tld
    name = Some Developer

Однако, данные коммита не поменялись, а значит, надо предпринять что-то ещё.

В простейшем случае можно удалить сделанный коммит через git reset HEAD~, заново добавить файлы и сделать новый коммит. Теперь коммит будет иметь нужного автора. Но если в коммите много файлов или так делать лень, то есть другой способ:

git commit --amend --author="Some Developer <developer@domain.tld>"

После этого автором коммита становится указанный человек.

вторник, 4 апреля 2023 г.

Регулярные выражения в postgresql

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

Исходные данные: таблица users следующего вида:

id | first_name | full_name | last_name | email ------------------------------------------------------------------- 1 | Иван | Иван Дмитриев | - | ivan@mail.ru 2 | Ольга | Ольга Корнева | - | ko@ya.ru 3 | Вячеслав | Вячеслав Домашнев | - | domv@yandex.ru 4 | Игорь | Игорь Черников | - | ichern@gmail.com 5 | Галина | Галина Горных | - | Galina@Yandex.ru

Начнем с простейшего - выберем все записи, где поле email содержит подстроку ya.

пятница, 17 июня 2022 г.

Константы и пространства имен в php

Сегодня поисследуем использование пространств имен (они же неймспейсы) для объявления констант и обращения к ним.

Как сказано в мануале:

классы (включая абстрактные и трейты), интерфейсы, функции и константы зависят от пространства имен.

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

Проверим:

namespace FirstNs {
    const SUPER_VALUE = 'NS-1';
}

namespace SecondNs {
    const SUPER_VALUE = 'NS-2';
}

namespace {
    const SUPER_VALUE = 'NS-GLOBAL';
    var_dump(
        SUPER_VALUE,
        FirstNs\SUPER_VALUE,
        SecondNs\SUPER_VALUE,
    );
}

// Вывод:
// string(9) "NS-GLOBAL"
// string(4) "NS-1"
// string(4) "NS-2"

Ровно то, что нужно.

Но что если мы хотим определить константы методом define.

Не проблема, пробуем написать так:

namespace FirstNs {
    define('SUPER_VALUE', 'NS-1');
}

namespace SecondNs {
    define('SUPER_VALUE', 'NS-2');
}

namespace {
    define('SUPER_VALUE', 'NS-GLOBAL');
    var_dump(
        SUPER_VALUE,
        FirstNs\SUPER_VALUE,
        SecondNs\SUPER_VALUE,
    );
}

Запускаем и видим:

Warning: Constant SUPER_VALUE already defined in ...
Warning: Constant SUPER_VALUE already defined in ...
Fatal error: Uncaught Error: Undefined constant "FirstNs\SUPER_VALUE" in ...

Судя по всему, мы пытаемся три раза определить одну и ту же константу в глобальном неймспейсе. Что же делать?

А просто указать нужный неймспейс при определении константы, благо php это позволяет:

namespace FirstNs {
    define('FirstNs\\SUPER_VALUE', 'NS-1');
    // или
    define(__NAMESPACE__ . '\\SUPER_VALUE', 'NS-1');
    
}

namespace SecondNs {
    define('SecondNs\\SUPER_VALUE', 'NS-2');
}

namespace {
    define('SUPER_VALUE', 'NS-GLOBAL');
    var_dump(
        SUPER_VALUE,
        FirstNs\SUPER_VALUE,
        SecondNs\SUPER_VALUE,
    );
}

// Вывод:
// string(9) "NS-GLOBAL"
// string(4) "NS-1"
// string(4) "NS-2"

Можно даже пойти дальше и в одном неймспейсе определить константу другого неймспейса:

namespace FirstNs {
    define('SecondNs\\SUPER_VALUE', 'NS-2-1');
}

namespace SecondNs {
    define('FirstNs\\SUPER_VALUE', 'NS-1-2');
}

Делать так в продакшене категорически не рекомендуется.

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

вторник, 8 февраля 2022 г.

Длина строк в Go

Этот пост родился из-за странного поведения валидатора Length из пакета ozzo-validation.

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

data := "Некоторая строка"
err := validation.Validate(data,
    validation.Required,
    validation.Length(10, 16),
)
fmt.Println(err)

И так как в строке у нас кириллические символы, и более того - кодировка строки utf-8, то валидация не пропускает строку, падая с ошибкой "the length must be between 10 and 16". Почему так происходит, ведь валидируемая строка содержит ровно 16 символов?

Для того, чтобы понять почему валидация выдает ошибку - придется сходить в исходный код валидатора. Поиски приводят к тому, что для определения длины валидируемой строки используется метод Len() из типа reflect#Value. Путешествие дальше приводит нас к тому, что свойство Len вычисляется с помощью встроенной функции len. А как сообщает мануал:

The len built-in function returns the length of v, according to its type:
String: the number of bytes in v.

Следовательно, валидация на основании длины строки в байтах нас не устраивает, так как в случае многобайтных кодировок (коей является utf-8) число символов в строке не равно числу байт в этой же строке.

Хотелось бы, чтобы был метод типа utfLen(). И он есть в пакете unicode/utf8, правда, с немного другим названием - RuneCountInString. И о чудо, данный метод даже используется в валидаторе, если установить дополнительный флаг rune. И это можно сделать применив валидатор RuneLength.

Обновленный код:

data := "Некоторая строка"
err := validation.Validate(data,
    validation.Required,
    validation.RuneLength(10, 16),
)
fmt.Println(err)

Валидация отрабатывает как и ожидается, выводя <nil>.

И не забудьте написать для этого случая тест (ну или хотя бы комментарий), чтобы пытливый программист, использующий код после вас, не смог бы заменить RuneLength на Length, думая, что "и так сойдёт".

P.S. Больше разъяснений про длины строк можно почитать в этом ответе на stackoverflow.

P.P.S. Отличное видео на Youtube, раскрывающее суть кодировок.

среда, 4 августа 2021 г.

Интерпретация переменных в строке

Сегодня еще один пост о всяких веселых способах написать код в php.

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

$number = 42;
echo "Number is $number";
// outputs "Number is 42"

Теперь зададимся вопросом - а можно ли как-то интерпретировать в строках какие-либо выражения? Например, сложение двух чисел. Естественно, написание в лоб приводит к ошибкам:

$a = 42;
$b = 24;
echo "Sum is {$a + $b}";

Что же делать? Понятно, что можно завести класс, объявить в нем метод суммирования, но это как-то много кода:

class Summator {
    public function sum($a, $b) {
        return $a + $b;
    }
}

$a = 42;
$b = 24;
$o = new Summator();
echo "Sum is {$o->sum($a, $b)}";
// outputs "Sum is 66"

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

$a = 42;
$b = 24;
$sum = function($a, $b) { return $a + $b; };
echo "Sum is {$sum($a, $b)}";
// outputs "Sum is 66"

А с учетом стрелочных функций (с версии 7.4), которые имеют доступ к переменным родительской области видимости, код упрощается до такого:

$a = 42;
$b = 24;
$sum = fn() => $a + $b;
echo "Sum is {$sum()}";
// outputs "Sum is 66"

Ну и наконец самая безумная версия:

$a = 42;
$b = 24;
echo "Sum is ${0 * ${0}=$a+$b}";
// outputs "Sum is 66"

Что же здесь происходит?

Здесь мы пытаемся вывести переменную с названием, которое получается как результат умножения 0 на переменную ${0}. А значение ${0} является суммой значений $a и $b. Почему php выполняет присваивание значения переменной при парсинге строки - не знаю. Но тем не менее, мы получаем, что переменной ${0} присваивается 66. А так как результатом присваивания является присвоенное значение, то 0 * ${0}=$a+$b превращается в 0 * 66, и выражение ${0 * ${0}=$a+$b} сворачивается в ${0}. То есть мы хотим вывести переменную с названием 0, которую мы уже определили ранее, как сумму $a и $b. Вот и получаем в выводе 66.

Спасибо за внимание и никогда не пишите такой код)

пятница, 18 июня 2021 г.

Ликбез по подсчету количества частей sms

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

Вводные данные: текст, который вы хотите отправить с помощью sms-сервиса, может быть достаточно большим, например, содержать 150-200 символов. Сотовый оператор (или другой sms-провайдер) не может отправить такой длинный текст как одну sms-ку, и потому разбивает ее на несколько частей. В зависимости от кодировки символов в тексте максимальная длина части разная. Отправка сообщения частями не значит, что получатель получит 2 разные sms-ки. В телефоне получателя части соединятся в одну sms-ку. Поэтому для получателя сообщение любой длины будет выглядеть как одна sms-ка.

А вот отправителю сложнее. Если вы используете какое-то стороннее API для отправки sms, то будьте уверены, что длинное сообщение будет отправлено частями. Таким образом, если вы снимаете со своего пользователя сколько-то денег за отправку сообщения, но при этом не учитываете факт отправки по частям, то баланс может не сойтись. Кстати, интересный факт - не все sms-провайдеры держат информацию о правилах деления на части в более-менее открытом доступе. У кого-то придется пошерстить мануал или FAQ, кому-то придется задать вопрос в саппорт. (Из личного опыта интеграции как минимум с 7-8 провайдерами, один из которых - заграничный).

В случае если в сообщении есть хотя бы один символ кириллицы - то максимальная длина одного сообщения составит 70 символов. Точнее даже не символов, а позиций (но об этом позже). То есть 70 и меньше символов будут отправлены как одна sms-ка. Если символов становится больше 70, то провайдер начнет разбивать сообщение на части. Обычно длина каждой части - 67 символов. 70 и 67 это общепринятые значения на данный момент, но лучше уточнить эти лимиты в документации оператора\провайдера.

То есть, если ваше сообщение состоит из 90 символов, то провайдер отправит его как две части: первая часть длиной 67 символов, вторая - 23 символа. Сообщение длиной 71 символ не удастся отправить как одно, тут тоже две части - 67 + 4. Так что либо укладывайтесь в 70, либо пишите еще больше текста, все равно за вторую часть придется платить

Переходим к сообщениям, написанным без использования кириллицы. Здесь лимит длины сообщения составляет уже 160 символов на одну смс и 153 символа на часть (хотя я встречал одного провайдера со 157 символами на часть). Расчет частей аналогичен: сообщение из 190 символов будет состоять из двух частей: 153 и 37 символов каждая.

Следующий важный момент: как определить какую из пар (70/67 или 160/153) использовать для расчета числа частей? Понятно, что если в тексте сообщения есть хотя бы один кириллический символ, то выбор очевиден - 70/67. В остальных случаях используем 160/153. Однако, копнем дальше - что делать, если у вас какой-нибудь многоязычный сервис и хочется корректно рассчитывать число частей для разных языков, даже с учетом национальных особенностей? Здесь на помощь приходит тот факт, что базовой кодировкой сообщений, отправляемых через sms, является GSM 03.38. Таблица символов данной кодировки содержит помимо некоторых ASCII-символов (цифры, латинский алфавит и некоторые знаки препинания) также символы, которые не относятся к ASCII. Пример: испанская ñ или немецкое ß хоть и не входят в ASCII-символы, но в смске займут один символ в отличие от кириллических символов.

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

Как следует из всего вышесказанного, для расчета числа частей надо написать собственный, не самый сложный код. Однако, можно использовать уже имеющиеся библиотеки. Для php можно использовать, например, мной же и написанную библиотеку sms-charset-detector, которая определяет кодировку сообщения. В зависимости от результата, вы можете выбрать какую пару чисел использовать. Для javascript можно использовать, например, sms-counter, здесь можно сразу увидеть число частей, на которое будет разбито сообшение.

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

понедельник, 3 мая 2021 г.

Приведение переменной к массиву

Сегодня краткий пост о том, как привести переменную какого-то типа к типу массив (array).

Итак, первый вариант, сразу приходящий в голову:

$var = 42;
$varArray = [$var];

Он хорош до тех пор, пока $var не является массивом. В случае с массивом мы получаем двумерный массив. Естественно, это нас не устраивает. Поэтому улучшаем конструкцию:

$var = 42;
if (!is_array($var)) {
    $var = [$var];
}

Но тут многовато строк, можно ли попроще?

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

$var = 42;
$varArray = (array) $var;
print_r($varArray);
// Вывод: Array
// (
//   [0] => 42
// )

$var = [42, 42];
$varArray = (array) $var;
print_r($varArray);
// Вывод: Array
// (
//   [0] => 42
//   [1] => 42
// )

На этом всё.

четверг, 11 марта 2021 г.

Doctrine: ошибка парсинга запроса

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

$conn = $this->getEntityManager()->getConnection();
$conn->executeQuery($query, [
    'param1' => 42,
    'param2' => 'forty-two',
    'param3' => '%some_other_data%',
]);

При выполнении этого кода ловим ошибку типа такой:

SQLSTATE[08P01]: <<Unknown error>>: 7 ERROR:  bind message supplies 0 parameters, but prepared statement "pdo_stmt_000...." requires 3

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

В качестве запроса, ломающего Доктрину можно привести такой (использую postgresql, текст запроса очень приблизительный):

SELECT
    id,
    value::FLOAT,
    exec_date
FROM (
    VALUES
    (
        8095,
        41,
        '2020-01-02 10:48:17'
    ),
    --- Таких values тут сотни две
) as vs (id, value, exec_date)
JOIN
    some_table t ON vs.id = t.foreign_id::INT
WHERE
   t.created_at::TIMESTAMP + interval '3600 second' <= vs.executed_at::TIMESTAMP
   AND t.field_one = :param1
   AND t.field_two = :param2
   --- и еще что-нибудь

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

среда, 17 февраля 2021 г.

Интеграция php-кода и html-верстки

Сегодня у нас урок для начинающих. Допустим, вы - начинающий программист, или верстальщик, или вообще пытались мимо проходить, но жизнь заставила соединить верстку (она же html-разметка) и php-код. Как это сделать с минимальными усилиями?

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

<div>
  <h1>Это заголовок списка новостей</h1>
  <div class="some-class">
    <h2>Заголовок новости 1</h2>
    <p>Небольшой блок текста для ознакомления</p>
    <a href="/detail.php?id=1">Читать текст целиком</a>
    <span class="author">Автор новости: И. Иванов</span>
  </div>
  <div class="some-class">
    <h2>Заголовок новости 2</h2>
    <p>Небольшой блок текста для ознакомления</p>
    <a href="/detail.php?id=2">Читать текст целиком</a>
    <span class="author">Автор новости: И. Иванов</span>
  </div>
  <div class="some-class">
    <h2>Заголовок новости 3</h2>
    <p>Небольшой блок текста для ознакомления</p>
    <a href="/detail.php?id=3">Читать текст целиком</a>
    <span class="author">Автор новости: И. Иванов</span>
  </div>
</div>

И есть массив новостей $newsArray, полученный из БД/API/откуда-то ещё. Мы не будем рассматривать, как вы получили этот массив, будем считать, что он у вас есть.

Для лучшего понимания, что и как нужно сделать, разобьем нашу задачу на несколько шагов.

среда, 30 декабря 2020 г.

Порционная обработка записей в цикле

Прямо вот совсем недавно на стеке возник вопрос - как порционно загружать записи из CSV в БД? Вопрос-то простой, но дополнительным условием было - после окончания цикла while нельзя делать дополнительный запрос на вставку.

Простое и понятное решение:

$handle = fopen(/* ... */);
$batch = [];
while (($data = fgetcsv($handle, 4096, ';')) !== false) {
  $batch[] = processData($data);

  if(100 === \count($batch)) {
    runBatchInsert($batch);

    $batch = [];
  }
}
fclose($handle);
if ($batch) {
    runBatchInsert($batch);
}

Как мы видим - здесь мы собираем записи в массив $batch, и как только в этом массиве будет 100 элементов - выполняем запрос на вставку в функции runBatchInsert(). Понятно, что после завершения цикла в $batch могут находиться данные, которые также надо вставить, что мы и делаем вторым вызовом функции runBatchInsert().

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

$handle = fopen(/* ... */);
$batch = [];
// запускаем "вечный" цикл
while (true) {
  $data = fgetcsv($handle, 4096, ';');
  // если данные есть - добавляем их в $batch
  if (false !== $data) {
    $batch[] = processData($data);
  }

  // если размер батча достиг лимита или мы больше не получили данных
  if(100 === \count($batch) || false === $data) {
    // в батче есть данные - вставляем
    if ($batch) {
      runBatchInsert($batch);
    }

    // данных больше нет - прерываем цикл
    if (false === $data) {
      break;    
    }

    // очищаем батч
    $batch = [];
  }
}
fclose($handle);

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

понедельник, 23 ноября 2020 г.

Загрузка файла при обновлении записи

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

Многие начинающие разработчики будут полагать, что путь к текущему файлу обложки надо выводить на форму редактирования, затем, аналогично полям <input type="text" />, добавлять его значение в запрос\метод\функцию обновления. По их мнению получается, что если файл не был обновлен, то в БД запишется то же самое значение пути к файлу, что есть в БД сейчас. Выглядит это в их голове как-то так:

if (isset($_POST['update-btn'])) {
    $name = $_POST['name'];

    // вроде как тут должен быть путь к файлу,
    // но <input type="file" /> работает не так
    $file = $_POST['file'];

    $query = "UPDATE tbl_book SET name = '$name', filepath = '$file' WHERE id = 42";
    $dbh->execute($query);
}

Как следствие, у них возникает вопрос - как вывести текущий путь к файлу в <input type="file" />? Ответ прост - никак. Нужно понять две вещи: первое - никакое значение от сервера в <input type="file" /> предустановить нельзя. Второе - работать с заменой файла следует иначе, чем с заменой текстовых значений.

Поэтому правильный алгоритм обновления файла выглядит так:

1. На форме обновления рядом с полем загрузки файла (<input type="file" />) выводим текущий файл, например: Текущая обложка <img src="path/to/file.jpg" />. Так как в общем случае никому не интересно, как файл назван в системе, то такого вывода достаточно: пользователь видит содержимое файла (собственно картинку) и может решить - следует ли загрузить новый файл. Если же действительно надо показать текущий путь к файлу, то выводим, например, Текущий файл расположен по пути "path/to/file.jpg". В обоих случаях path/to/file.jpg - это путь к файлу, хранящийся в БД.

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

2.1. Пользователь не загрузил файл. Значит, в запросе\методе\функции обновления записи не требуется указывать поле с файлом.

2.2. Пользователь загрузил новый файл. Значит, надо этот файл сохранить в каком-то каталоге и сформировать путь к нему. Далее, в запросе\методе\функции обновления записи указать путь к новому файлу, и после успешного обновления записи удалить с диска старый файл. Старый файл можно и не удалять, если на диске достаточно места.

Схематично код выглядит так, его аналоги можно реализовать в любом фреймворке:

if (isset($_POST['update-btn'])) {
    $name = $_POST['name'];

    if ($_FILES['file']) {
        move_uploaded_file();
        $file = 'path/to/file.jpg';

        // опционально, за этим значением нужно сходить в БД
        $oldFile = 'path/to/old_file.jpg';
    }

    if (isset($file)) {
        $query = "UPDATE tbl_book SET name = '$name', filepath = '$file' WHERE id = 42";
    } else {
        $query = "UPDATE tbl_book SET name = '$name' WHERE id = 42";
    }
    
    $dbh->execute($query);
    
    // опционально
    unlink($oldFile);
}

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

понедельник, 26 октября 2020 г.

Short circuit evaluation в php

Сегодня поближе познакомимся с short circuit evaluation: выясним что это за зверь такой, посмотрим примеры и выясним, как он нам может помочь.

Для начала немного теории. Short circuit evaluation (не могу предложить простого русского перевода) - это стратегия в языках программирования, которая используется, чтобы избежать ненужных вычислений.
Лучше всего это понять на примере булевых выражений. Допустим, мы проверяем условие вида if (checkSomething() && checkSomethingElse()). Если checkSomething() вернет false, то true в итоге уже никак не получить, следовательно, вычислять второе значение в checkSomethingElse() не имеет смысла - любое вычисленное значение никак не повлияет на итоговый результат.

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

Размеры таблиц и индексов в БД PostgreSQL

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

Перед тем как начать - рассмотрим вспомогательную функцию pg_size_pretty(), которая:

Преобразует размер в байтах, представленный в 64-битном целом, в понятный человеку формат с единицами измерения

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

select pg_size_pretty(100250408::bigint);
-- Вывод:
pg_size_pretty
text
--------------
96 MB

Теперь, вооружившись этой функцией, переходим к основным функциям.

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

select pg_size_pretty(pg_relation_size('my_table'));
-- Вывод (небольшая тестовая таблица в моей БД):
pg_size_pretty
text
--------------
48 kB

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

select pg_size_pretty(pg_indexes_size('my_table'));
-- Вывод (опять же для некоей тестовой таблицы):
pg_size_pretty
text
--------------
88 kB

Функция pg_total_relation_size() определяет сколько места суммарно занимает таблица, ее индексы и данные TOAST, так что ее результат будет отличаться от суммы результатов двух предыдущих запросов.

Ну и чтобы не складывать на калькуляторе размеры всех таблиц и индексов - размер базы данных PostgreSQL определим с помощью pg_database_size():

select pg_size_pretty(pg_indexes_size('my_table'));
-- Вывод (опять же для некоей тестовой базы данных):
pg_size_pretty
text
--------------
8137 kB

Больше информации про описанные функции - здесь: https://postgrespro.ru/docs/postgrespro/12/functions-admin#FUNCTIONS-ADMIN-DBOBJECT

понедельник, 1 июня 2020 г.

Неочевидное поведение функций конвертации даты

Продолжаем изучать неочевидное поведение функций php.

Рассмотрим такой код:

$var = strtotime(date('d.m.Y', time()));

Кажется, что здесь выполняется лишняя работа. Сначала мы берем таймштамп, возвращаемый функцией time(), и на его основе получаем строку времени определенного формата. Далее мы преобразуем полученную строку времени обратно в таймштамп. И логично, что таймштампы должны совпадать. Но нет.

Неочевидность в том, что строка времени в формате "d.m.Y" преобразуется в таймштамп начала дня, то есть "d.m.Y" аналогичен "d.m.Y 00:00:00".

Добавляем немного вывода и видим:

$ts = time();

$var = strtotime(date('d.m.Y', $ts));

var_dump(date('d.m.Y H:i:s', $ts));    // string(19) "01.06.2020 22:36:09"
var_dump(date('d.m.Y H:i:s', $var));   // string(19) "01.06.2020 00:00:00"

$varDayStart = strtotime(date('d.m.Y 00:00:00', $ts));
var_dump($var === $varDayStart);    // bool(true)


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

пятница, 10 апреля 2020 г.

Уникальные или неуникальные значения в массиве

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

Сразу к примеру:

$array = [1, 2, 3, 4, 3, 4, 5, 6, 6];

// Для любого из вариантов задачи нам потребуется знать
// сколько раз в массиве встречается каждое значение
$freqs = array_count_values($array);

// Массив уникальных значений - [1, 2, 5]
$unique = array_keys(
    array_filter(
        $freqs,
        function ($freq) { return 1 === $freq; }
    )
);

// Массив неуникальных значений - [3, 4, 6]
$nonunique = array_keys(
    array_filter(
        $freqs,
        function ($freq) { return 1 < $freq; }
    )
);

// Фильтрация исходного массива с оставлением только повторяющихся значений -[3, 4, 3, 4, 6, 6]
$allNonunique = array_filter(
    $array,
    function ($v) use ($freqs) { return 1 < $freqs[$v]; }
);


В общем-то всё, на досуге можете расширить этот код и задать количество появлений в виде переменной, а не фиксированного значения 1.

понедельник, 6 апреля 2020 г.

Неявное поведение DateTime::createFromFormat

Данный пост есть результат недавнего обсуждения в одном из php-каналов некоторого странного (как кажется изначально) поведения функции DateTime::createFromFormat.

Рассмотрим простейший код, надо отметить, что исследуемое поведение отмечается только 31-го числа каждого месяца (ну и плюс 29-30 для февраля):

$months = [1, 2, 3, 4];

foreach ($months as $month) {
    $dt = '2019-' . $month;
    echo $dt . ': ' . (\DateTime::createFromFormat('Y-m', $dt))->format('Y-m-d') . PHP_EOL;
}


Обратите внимание, что при создании объекта не указывается день. Так как день не указан, то разумно предположить (это же подтверждается в комментариях), что php берет в качестве дня текущий день запуска скрипта из системных настроек.

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

// ожидаемо, в январе есть 31 число
2019-1: 31.01.2019
// в феврале-2019 нет 29 (и 30 и 31) числа, потому 31-му февраля
// соответствует третье марта (а 29-му февраля - первое марта)
2019-2: 03.03.2019
// ожидаемо, в марте есть 31 число
2019-3: 31.03.2019
// в апреле нет 31 числа, и следующим после 30 апреля идет 1 мая
2019-4: 01.05.2019


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

Можно, конечно, рассуждать о том, что раз день не указан, то может надо кидать эксепшен при создании объекта, но так как такого поведения нет, то придерживаемся мудрого принципа "Явное лучше, чем неявное" и повнимательней пишем свой код.

Всем здоровья)

воскресенье, 8 марта 2020 г.

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

В предыдущих частях (часть 1, часть 2) мы рассмотрели как к строковому типу приводятся скалярные типы данных и null. Сейчас перейдем к таким типам данных как массив, объект и ресурс.

Массив, сразу смотрим пример:

$var = [1,2,3];
$strConcat = 'Эту строку я конкатенирую с переменной ' . $var;
$strEval = "В эту строку я подставляю переменную $var";
echo $var;
echo PHP_EOL;
echo $strConcat;
echo PHP_EOL;
echo $strEval;
// вывод:
Notice: Array to string conversion in file on line ..
Notice: Array to string conversion in file on line ..
Notice: Array to string conversion in file on line ..
Array
Эту строку я конкатенирую с переменной Array
В эту строку я подставляю переменную Array


Итак, строковое представление массива это просто слово Array. И так как это не ожидаемая операция над массивом, то выводится еще и Notice. Таким образом, просто взять и вывести массив на экран не получится. Можно обойти массив с помощью цикла и вывести элементы в нужном формате. Естественно, если элементы тоже массивы - их также требуется обойти циклом. И так далее. В случае если массив одномерный - можно воспользоваться функцией implode. Также, если у вас при вставке в БД в поле появляется слово Array - вы поняли, в чем ошибка.

Объект. С объектами ситуация следующая: не всякий объект можно вывести на экран. Например, возьмем такой класс и попытаемся вывести на экран объект данного класса:

class myStdClass
{
    public $intField;
    public $strField;
}

$var = new myStdClass();
$var->intField = 42;
$var->strField = 'Forty two';
$strConcat = 'Эту строку я конкатенирую с переменной ' . $var;
$strEval = "В эту строку я подставляю переменную $var";
echo $var;
echo PHP_EOL;
echo $strConcat;
echo PHP_EOL;
echo $strEval;
// вывод:
Recoverable fatal error: Object of class stdClass could not be converted to string in ... on line ...


Получаем фатальную ошибку, так как php не представляет как привести данный объект к строковому представлению. Но ситуация поправима. Для приведения к объекта к строке требуется определить в классе "магический" метод __toString. Метод, естественно, должен вернуть некую строку, которая и будет строковым представлением объекта.
Модифицируем класс из предыдущего примера:

class myStdClass
{
    public $intField;
    public $strField;

    public function __toString(): string
    {
        return 'intField: ' . $this->intField . '; strField: ' . $this->strField;
    }
}

$var = new myStdClass();
$var->intField = 42;
$var->strField = 'Forty two';
$strConcat = 'Эту строку я конкатенирую с переменной ' . $var;
$strEval = "В эту строку я подставляю переменную $var";
echo $var;
echo PHP_EOL;
echo $strConcat;
echo PHP_EOL;
echo $strEval;
// вывод:
intField: 42; strField: Forty two
Эту строку я конкатенирую с переменной intField: 42; strField: Forty two
В эту строку я подставляю переменную intField: 42; strField: Forty two


Как видим, теперь наш объект прекрасно приводится к строковому представлению.

Я не знаю ни одного случая, когда требуется строковое представление ресурса. Но для полноты картины посмотрим на следующий код:

$var = fopen('/tmp/1.tmp', 'a');
$strConcat = 'Эту строку я конкатенирую с переменной ' . $var;
$strEval = "В эту строку я подставляю переменную $var";
echo $var;
echo PHP_EOL;
echo $strConcat;
echo PHP_EOL;
echo $strEval;
// вывод:
Resource id #5
Эту строку я конкатенирую с переменной Resource id #5
В эту строку я подставляю переменную Resource id #5


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

На этом я завершаю цикл статей по преобразованию типов данных в строки, всем спасибо за внимание.

воскресенье, 23 февраля 2020 г.

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

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

Рассмотрим тип данных int. Переменная типа int может быть задана в четырех системах счисления: десятичной, восьмеричной, шестнадцатеричной и двоичной. Независимо от системы счисления, вывод будет преобразован к десятичной системе счисления:

$a = 0;
$b = 42;
$c = 0b101011;   // 43 в десятичной
$d = 054;        // 44 в десятичной
$e = 0x2D;       // 45 в десятичной
$f = -42;

echo $a . PHP_EOL;
echo $b . PHP_EOL;
echo $c . PHP_EOL;
echo $d . PHP_EOL;
echo $e . PHP_EOL;
echo $f . PHP_EOL;
// вывод:
0
42
43
44
45
-42


Числа, превышающие размер типа int (PHP_INT_MAX / PHP_INT_MIN), автоматически конвертируются в тип float и выводятся соответствующе:

$a = PHP_INT_MAX;
$b = 2 + PHP_INT_MAX;
$c = PHP_INT_MIN;
$d = PHP_INT_MIN -1;

echo $a . PHP_EOL;
echo $b . PHP_EOL;
echo $c . PHP_EOL;
echo $d . PHP_EOL;
// вывод:
9223372036854775807
9.2233720368548E+18
-9223372036854775808
-9.2233720368548E+18


Для того чтобы вывести int переменную не в десятичной, а другой системе счисления, нужно воспользоваться функциями форматирования, например, sprintf/printf:

$a = 42;

echo sprintf("%o", $a) . PHP_EOL;  // восьмеричная
echo sprintf("%x", $a) . PHP_EOL;  // шестнадцатиричная, буквы в нижнем регистре
echo sprintf("%X", $a) . PHP_EOL;  // шестнадцатиричная, буквы в верхнем регистре
echo sprintf("%b", $a) . PHP_EOL;  // двоичная
// вывод:
52
2a
2A
101010


Переходим к выводу переменных типа float.

$floatNums = [
    // Первый способ записи float-чисел
    4.2,
    4.2222,
    4.22222222,
    0.2,
    0.0002,
    0.00002,
    0.000002,
    // Второй способ записи float-чисел
    1.2e4,
    // Третий способ записи float-чисел
    1.2E4,
    5E-3,
    5E-4,
    5E-5,
    // int значения превышающие размер типа int конвертируются во float
    PHP_INT_MAX + 20,
];

foreach ($floatNums as $num) {
    echo $num . PHP_EOL;
}
// вывод:
4.2
4.2222
4.22222222
0.2
0.0002
2.0E-5    //числа с пятью и более знаками после запятой выводятся в нотации с основанием и мантиссой
2.0E-6
12000
12000
0.005
0.0005
5.0E-5
9.2233720368548E+18


Как видим - числа выводятся с тем же количеством знаков после запятой, что были указаны при их объявлении. Для указания нужного числа знаков после запятой - пользуйтесь функциями форматирования, например, теми же sprintf/printf или number_format:

$floatNum = 4.23456789;
echo sprintf('%f', $floatNum) . PHP_EOL;
echo sprintf('%.2f', $floatNum) . PHP_EOL;
echo sprintf('%.5f', $floatNum) . PHP_EOL;
echo sprintf('%.12f', $floatNum) . PHP_EOL;

echo number_format($floatNum) . PHP_EOL;
echo number_format($floatNum, 2) . PHP_EOL;
echo number_format($floatNum, 5) . PHP_EOL;
echo number_format($floatNum, 6) . PHP_EOL;

// вывод
4.234568   // по умолчанию выводится 6 знаков после запятой
4.23
4.23457    // можно подумать что это округление, но нет, такой вывод связан с представлением числа в памяти программы
4.234567890000     // оставшиеся позиции заменяются нулями
4
4.23
4.23457    // аналогично, это НЕ округление
4.234568


Оставшиеся типы данных - массив, объект и ресурс, рассмотрим в заключительном посте.

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

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

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

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

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