[Из песочницы] Аргументы функций в виде битовых констант в PHP

habr.png

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

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

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


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

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

Не стоит путать оператор | (побитовое «ИЛИ») с часто используемым оператором || (логическое «ИЛИ»), который обычно встречается в конструкциях 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;

Аналогичным образом оператор & (побитовое «И») не стоит путать с часто применяемым оператором && (логическое «И»).
Побитовое «И» —  это бинарная операция, действие которой эквивалентно применению логического «И» к каждой паре битов, находящихся на одинаковых позициях в двоичных представлениях операндов. Другими словами, если оба соответствующих бита операндов равны 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

Стоит отметить, что в 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.
  • Вместо этого вы можете передать ассоциативный массив в качестве аргумента, чтобы иметь возможность устанавливать небулевые значения (или даже класс).
  • Существуют веские причины, по которым вам следует избегать использования логических «флагов функций» в качестве аргументов и вместо этого использовать другую функцию или метод для изменяемой функциональности.

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


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

© Habrahabr.ru