Аргументы функций в виде битовых констант в PHP +6


Привет, Хабр! Представляю вашему вниманию перевод статьи Лиама Хамметта (Liam Hammett): Bitmask Constant Arguments in PHP.

PHP содержит множество стандартных функций, которые принимают аргументы логического типа (boolean) в форме встроенных констант со значениями двоичных чисел.
Эти значения комбинируются в единый аргумент функции с целью передачи нескольких булевых флагов компактным образом.


Они могут работать немного не так, как многие представляют и используют в своем коде, поэтому предлагаю рассмотреть, как на самом деле это устроено.




Как это используется в функциях PHP


PHP 7.2, включая расширения, содержит свыше 1800+ предопределённых констант и часть из них используются как аргументы функций.
Один из примеров такого применения? — ?это новая опция в PHP 7.3, которая позволяет бросать исключение из функции json_encode при ошибках преобразования.


json_encode($value, JSON_THROW_ON_ERROR);

Используя | (или) побитовый оператор, несколько аргументов функции работают как один. Пример из PHP документации:


json_encode($value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE);

Очень даже ничего.


Но все таки, как же это работает?


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


Целые числа могут быть указаны в десятичной (основание 10), шестнадцатеричной (основание 16), восьмеричной (основание 8) или двоичной (основание 2) системе счисления. […] 
Для записи в двоичной системе счисления, необходимо поставить перед числом 0b. 
php.net

Примеры констант с разным набором двоичных чисел:


const A = 0b0001; // 1
const B = 0b0010; // 2
const C = 0b0100; // 4
const D = 0b1000; // 8

Обратите внимание на пример и последовательность чисел. Каждое двоичное значение представляет значение в два раза выше для каждого нуля на конце. Нули между 0b и 1 необязательны, но могут помочь выровнять исходный код.


К счастью, нам необходимо понимать, как работают только две побитовые операции.


Побитовое «ИЛИ» (OR)


Не стоит путать оператор | (побитовое «ИЛИ») с часто используемым оператором || (логическое «ИЛИ»), который обычно встречается в конструкциях if else.
Побитовое «ИЛИ»? — ?это бинарная операция, действие которой эквивалентно применению логического «ИЛИ» к каждой паре битов, находящихся на одинаковых позициях в двоичных представлениях операндов. Другими словами, если оба соответствующих бита операндов равны 0, двоичный разряд результата равен 0; если же хотя бы один бит из пары равен 1, двоичный разряд результата равен 1.


Пример побитовой операции «ИЛИ»:


const A     = 0b0001;
const B     = 0b0010;
const C     = 0b0100;
const D     = 0b1000;
A | B     === 0b0011;
A | C | D === 0b1101;

Побитовое «И» (AND)


Аналогичным образом оператор & (побитовое «И») не стоит путать с часто применяемым оператором && (логическое «И»).
Побитовое «И»? — ?это бинарная операция, действие которой эквивалентно применению логического «И» к каждой паре битов, находящихся на одинаковых позициях в двоичных представлениях операндов. Другими словами, если оба соответствующих бита операндов равны 1, результирующий двоичный разряд равен 1; если же хотя бы один бит из пары равен 0, результирующий двоичный разряд равен 0.


const A     = 0b0001;
const B     = 0b0010;
const C     = 0b0100;
const D     = 0b1000;
const VALUE = 0b1010;
A & B     === 0b0000; // нет совпадений единичных битов в A и B
A & C & D === 0b0000; // нет совпадений единичных битов в A, B или C
A & A     === 0b0001; // тот же бит устанавливается в A дважды
A & VALUE === 0b0000; // нет совпадений единичных битов в A и VALUE
B & VALUE === 0b0010; // бит 1 встречается как в B, так и в VALUE

Может ли число иметь логический тип (boolean)?


Стоит отметить, что в PHP существует понятие «манипуляции с типами» (type juggling). На языке неспециалистов это значит, что он (PHP) автоматически попытается преобразовать данные одного типа к другому, если это потребуется.
Если вы понимаете, как происходят такие преобразования, то это может быть полезным инструментом.
Например, мы знаем, что число 0 выступает в качестве false при преобразовании к логическому типу (boolean), в то время как все остальные числа будут true. Помните, что эти двоичные значения, с которыми мы работаем, на самом деле являются целыми числами?


Подведем итоги!


Теперь мы можем объединить это знание, чтобы создать конструкцию if, код в которой будет выполняться только в том случае, если результат побитовой операции между числами не будет 0b0000 (или 0, который преобразовывается в false).


const A = 0b0001;
const B = 0b0010;

function f($arg = 0)
{
    if ($arg & A) {
        echo 'A';
    }
    if ($arg & B) {
        echo 'B';
    }
}

f();        // nothing
f(A);       // 'A'
f(B);       // 'B'
f(A | B);   // 'AB'

По такому же принципу работают другие встроенные функции PHP (например json_encode).


Стоит ли оно того?


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


  • Вы не можете передавать не булевые значения через аргумент.
  • Нет стандартного способа документировать аргументы с помощью docblocks.
  • Вы теряете общие подсказки и поддержку большинства IDE.
  • Вместо этого вы можете передать ассоциативный массив в качестве аргумента, чтобы иметь возможность устанавливать небулевые значения (или даже класс).
  • Существуют веские причины, по которым вам следует избегать использования логических «флагов функций» в качестве аргументов и вместо этого использовать другую функцию или метод для изменяемой функциональности.

Тем не менее, теперь вы знаете как это делается.




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




К сожалению, не доступен сервер mySQL