Pull to refresh

Bit Mask Resurrection

Reading time 3 min
Views 5.5K
По мотивам топиков:
Упаковка булевых переменных для хранения и поиска в базе
Хранение набора чекбоксов в одном поле БД. Битовая маска.
В этих топиках была похоронена замечательная идея. Что ж, попробуем её возродить ещё раз…


Начну с примера. Пользователь должен иметь доступ к определённым функциям сайта. Всего этих функций, к примеру, 1000. Есть группы, к которым привязывается определённый набор функций. Пользователь может принадлежать сразу нескольким группам. За один запрос к сайту, может выполниться сразу несколько функций. Довольно реальная задача, не так ли?

Группы будем хранить в таблице groups. Для хранения набора функций к группе, самое очевидное решение, создать связывающую таблицу:
CREATE TABLE `functions_to_group` (
`group_id` int(4) unsigned NOT NULL,
`function_id` int(4) NOT NULL
UNIQUE KEY `f_to_g` (`group_id`,`function_id`)
)


Теперь при запросе к функции нужно сделать запрос к базе:
"SELECT
`function_id`
FROM
`functions_to_group`
WHERE
`group_id` in (" .implode(",", $user_groups). ") and
`function_id` = " .$current_function_id;


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

Можно одним махом, т.е. запросом, получить все доступные функции и тягать за собой массив из нескольких сот элементов:
"SELECT
`group_id`,
`function_id`
FROM
`functions_to_group`
WHERE
`group_id` in (" .implode(",", $user_groups). ")"


Как-то некрасиво… Всё, что нам нужно, это проверка, есть ли в наборе данных определённое значение. Вот тут битовая маска как нельзя кстати. Но хранить маску в целом числе теперь не вариант. У нас ведь 1000 функций.
Нафик числа, нам нужны бинарные данные, ну так вот и есть такая функция pack(), как раз нам сойдёт.
Пишем простенький класс:
class BitMask
{
function __construct(){}

function GetMask($num)
{
// если 0, то возвращаем нулевой байт
if (!$num)
{
return "\0";
}

// само число определяет позицию единичного бита в бинарной строке
$num--;
//определяем кол-во нулевых байт, предшествующих значащему биту
$null_bytes = floor($num / 8);
//выставляем единичный бит в значащем байте...
$number = pow(2, $num % 8);

//и пакуем всё это дело
return pack('@' .$null_bytes. 'C', $number);
}

function InMask($needle, $findIn)
{
//проверяем, есть ли в строке искомое значение,
//т.е. побитово умножаем и отрубаем нулевые биты
return (bool)strlen(trim($needle & $findIn, "\0"));
}

}


Теперь пакуем весь набор функций для группы в одну строку, например, вот так:
$x = new BitMask();

$group_access = $x->GetMask(0);

while (list($k, $v) = each($_POST['functions_to_group']))
{
$group_access |= $x->GetMask($v);
}


Всё. На выходе получаем одну бинарную строку, которую можно хранить в таблице groups. При авторизации на сайте делаем один запрос к таблице groups:
"SELECT
`functions_bitmask`
FROM
`groups`
WHERE
`group_id` in (" .implode(",", $user_groups). ")"


и побитово складываем наборы всех групп пользователя:
$user_private_access = $x->GetMask(0);

while (list($k, $v) = each($user_groups_access))
{
$user_private_access |= $v;
}


Для проверки, есть ли доступ к функции, делаем так:
if ($x->InMask($x->GetMask($function_id), $user_private_access))
{
// есть доступ
}
else
{
// нет доступа
}


максимальный размер бинарной строки = ceil(max_function_id / 8) байт,
в нашем случае для 1000 элементов, максимальный размер будет 125 байт.

В строке длиной 255 символов, max_function_id = 2040.

Tags:
Hubs:
+10
Comments 36
Comments Comments 36

Articles