Bitmask in PHP for settings?

Bit fields are a very handy and efficient tool for dealing with flags or any set of boolean values in general.

To understand them you first need to know how binary numbers work. After that you should check out the manual entries on bitwise operators and make sure you know how a bitwise AND, OR and left/right shift works.

A bit field is nothing more than an integer value. Let's assume our bit field's size is fixed and only one byte. Computers work with binary numbers, so if the value of our number is 29, you'll actually find 0001 1101 in the memory.

Using bitwise AND (&) and bitwise OR (|) you can read out and set each bit of the number individually. They both take two integers as input and perform an AND/OR on each bit individually.

To read out the very first bit of your number, you could do something like this:

  0001 1101 (=29, our number)
& 0000 0001 (=1, bit mask)
= 0000 0001 (=1, result)

As you can see you need a special number where only the bit we're interested in is set, that's the so called "bit mask". In our case it's 1. To read out the second bit we have to "push" the one in the bitmask one digit to the left. We can do that with the left shift operator ($number << 1) or by multiplying our by two.

  0001 1101
& 0000 0010
= 0000 0000 (=0, result) 

You can do that for every bit in our number. The binary AND of our number and the bit mask leads either to zero, which means the bit wasn't "set", or to a non-zero integer, which means the bit was set.

If you want to set one of the bits, you can use bitwise OR:

  0001 1101
| 0010 0000 (=32, bit mask)
= 0011 1101 (=29+32)

However, you'll have to go a different way when you want to "clear" a bit.

A more general approach would be:

// To get bit n
$bit_n = ($number & (1 << $n)) != 0
// Alternative
$bit_n = ($number & (1 << $n)) >> $n

// Set bit n of number to new_bit
$number = ($number & ~(1 << $n)) | ($new_bit << $n)

At first it might look a bit cryptic, but actually it's quite easy.

By now you probably found out that bit fields are quite a low-level technique. That's why I recommend not to use them within PHP or databases.. If you want to have a bunch of flags it's probably ok, but for anything else you really don't need them.

The class you posted looks a bit special to me. For example, things like ... ? true : false are veery bad practice. If you want to use bit fields, you're probably better off defining some constants and use the method described above. It's not hard to come up with a simple class.

define('PERM_READ', 0);
define('PERM_WRITE', 1);

class BitField {
    private $value;

    public function __construct($value=0) {
        $this->value = $value;
    }

    public function getValue() {
        return $this->value;
    }

    public function get($n) {
        return ($this->value & (1 << $n)) != 0;
    }

    public function set($n, $new=true) {
        $this->value = ($this->value & ~(1 << $n)) | ($new << $n);
    }
    public function clear($n) {
        $this->set($n, false);
    }
}


$bf = new BitField($user->permissions);

if ($bf->get(PERM_READ)) {
    // can read
}

$bf->set(PERM_WRITE, true);
$user->permissions = $bf->getValue();
$user->save();

I didn't try any piece of code of this answer, but it should get you started even if it isn't working out of the box.

Note that you're limited to 32 values per bit field.


Here's how to define bitmasks.

// the first mask.  In binary, it's 00000001
define('BITWISE_MASK_1', 1 << 0); // 1 << 0 is the same as 1

// the second mask.  In binary, it's 00000010
define('BITWISE_MASK_2', 1 << 1);

// the third mask.  In binary, it's 00000100
define('BITWISE_MASK_3', 1 << 2);

To check if a bitmask is present (in this case in a function argument), use the bitwise AND operator.

function computeMasks($masks) {
    $masksPresent = array();
    if ($masks & BITWISE_MASK_1)
        $masksPresent[] = 'BITWISE_MASK_1';
    if ($masks & BITWISE_MASK_2)
        $masksPresent[] = 'BITWISE_MASK_2';
    if ($masks & BITWISE_MASK_3)
        $masksPresent[] = 'BITWISE_MASK_3';
    return implode(' and ', $masksPresent);
}

This works because when you OR two bytes (say, 00000001 and 00010000), you get the two together: 00010001. If you AND the result and the original mask (00010001 and say, 00000001), you get a the mask if it's present (in this case 00000001). Otherwise, you get zero.