15

Bits and bitmask are something I have been struggling to understand for a while, but I would like to learn how to use them for settings and things like that in PHP.

I have finally found a class that claims to do exactly that, and as I can tell, it seems to work, but I am not sure if it is the best way of doing this. I will post the class file with example code below to show it in working order.

Please if you have experience, tell me if it can be improved, for performance or anything else. I really want to learn this, and I have been reading up on it, but it is a difficult one for me to grasp so far.

The class...

<?php
    class bitmask
    {
        /**
         * This array is used to represent the users permission in usable format.
         *
         * You can change remove or add valuesto suit your needs.
         * Just ensure that each element defaults to false. Once you have started storing
         * users permsisions a change to the order of this array will cause the
         * permissions to be incorectly interpreted.
         *
         * @type Associtive array
         */
        public $permissions = array(
                                    "read" => false,
                                    "write" => false,
                                    "delete" => false,
                                    "change_permissions" => false,
                                    "admin" => false
                                    );

        /**
         * This function will use an integer bitmask (as created by toBitmask())
         * to populate the class vaiable
         * $this->permissions with the users permissions as boolean values.
         * @param int $bitmask an integer representation of the users permisions.
         * This integer is created by toBitmask();
         *
         * @return an associatve array with the users permissions.
         */
        public function getPermissions($bitMask = 0)
        {
            $i = 0;
            foreach ($this->permissions as $key => $value)
            {
                $this->permissions[$key] = (($bitMask & pow(2, $i)) != 0) ? true : false;

                // Uncomment the next line if you would like to see what is happening.
                //echo $key . " i= ".strval($i)." power=" . strval(pow(2,$i)). "bitwise & = " . strval($bitMask & pow(2,$i))."<br>";
                $i++;
            }
            return $this->permissions;
        }

        /**
         * This function will create and return and integer bitmask based on the permission values set in
         * the class variable $permissions. To use you would want to set the fields in $permissions to true for the permissions you want to grant.
         * Then call toBitmask() and store the integer value.  Later you can pass that integer into getPermissions() to convert it back to an assoicative
         * array.
         *
         * @return int an integer bitmask represeting the users permission set.
         */
        function toBitmask()
        {
            $bitmask = 0;
            $i = 0;
            foreach ($this->permissions as $key => $value)
            {

                if ($value)
                {
                    $bitmask += pow(2, $i);
                }
                $i++;
            }
            return $bitmask;
        }
    }
?>

How do I set/save the permissions as a bitmask value?

<?php
    /**
     * Example usage
     * initiate new bitmask object
     */
    $perms = new bitmask();

    /**
     * How to set permissions for a user
     */
    $perms->permissions["read"] = true;
    $perms->permissions["write"] = true;
    $perms->permissions["delete"] = true;
    $perms->permissions["change_permissions"] = true;
    $perms->permissions["admin"] = false;

    // Converts to bitmask value to store in database or wherever
    $bitmask = $perms->toBitmask();  //in this example it is 15
    $sql = "insert into user_permissions (userid,permission) values(1,$bitmask)";
    echo $sql; //you would then execute code to insert your sql.
?>

Example of taking the bitmask value and returning a true/false for each array item based on the bit value....

<?php
    /**
     * Example usage to get the bitmask value from database or session/cache.... then put it to use.
     * $permarr returns an array with true/false for each array value based on the bit value
     */
    $permarr = $perms->getPermissions($bitmask);

    if ($permarr["read"])
    {
        echo 'user can read: <font color="green">TRUE</font>';
    }
    else {
        echo 'user can read: <font color="red">FALSE</font>';
    }

    //user can WRITE permission
    if ($permarr["write"])
    {
        echo '<br>user can write: <font color="green">TRUE</font>';
    }
    else {
        echo '<br>user can write: <font color="red">FALSE</font>';
    }
?>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
JasonDavis
  • 48,204
  • 100
  • 318
  • 537
  • 1
    I know this isn't what you asked, but given that you're saving this to a database, and saving it as a single value, I gotta ask, why? You've made that column unusable by your database for any filtering. Are you concerned about space? – Marvo Mar 16 '11 at 00:04
  • 4
    i don't see why you say that the column is unusable. the way he did it he can easily retrieve the users who have the read permission by doing this for example : `select userid from user_permissions where (permission & 1) = 1` – grandouassou Mar 16 '11 at 00:14
  • @florian Not unusable, but would you prefer a bitmask over a column for example with `can_read`? I think I would prefer the latter, but interested in what other people think... – alex Mar 16 '11 at 00:19
  • 1
    sure `can_read` would be more explicit but if you have more settings maybe you don't want 15 columns with `can_boo`, `can_zoo`. – grandouassou Mar 16 '11 at 00:24
  • @florian I'm probably going to show my stupidity, but can't a bitmask only have 8 on/off values? – alex Mar 16 '11 at 00:26
  • it depends on how you store it. if your database column has a double type you can have up to 32 on/off values. I never needed to go over 32 settings but I think it may be possible to go up to 64 if the column type can store the 2^64 number... – grandouassou Mar 16 '11 at 00:40

2 Answers2

33

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.

svens
  • 11,438
  • 6
  • 36
  • 55
  • I have been playing with this some more and it is perfect, amazing every single example I found online used pow(2, $i) and I have been told that isn't right or the best so this is really great, thank you so much, i'm sure it will help others too – JasonDavis Mar 16 '11 at 10:25
  • Thanks for the feedback :). As you said, I hope it helps others too, since the answer got a bit longer than expected.. – svens Mar 20 '11 at 12:35
  • 1
    Also if you feel like further improving the example, you should replace the `define()`s with `const` and maybe implement some automagic getters/setters or ArrayAccess for extra awesomness. – svens Mar 20 '11 at 12:39
  • @svens That is what I was thinking of changing it to be more self contained, not sure what you mean by ArrayAccess though – JasonDavis Mar 21 '11 at 04:53
  • @jasondavis Check out this manual page: http://php.net/manual/en/class.arrayaccess.php. It makes your object act like an array, so you could do something like `$bf[PERM_READ]` to get the value. – svens Mar 21 '11 at 09:34
  • @svens Again, thank you so much for your help, I have chosen your answer here but I am really wanting to perfect this class and you gave me a GREAT starting point but again I am looking for the help of others who can create a better end result then I can. If you are interested in checking out my new question wiht this same class you created, http://stackoverflow.com/questions/5380506/improve-this-php-bitfield-class-for-settings-permissions there is a 200 point bounty on it, hoping to get more good ideas and stuff from others, feel free to contribute if you have the time +11111 – JasonDavis Mar 24 '11 at 02:09
  • Your class is simply amazing. You should rest assured that you've made the world a little better with it! – Theodore R. Smith Jan 18 '12 at 18:36
  • @svens, because of all code on SO being licensed under the very restrictive and viral CC-BY-SA license (http://meta.stackexchange.com/questions/25956/what-is-up-with-the-source-code-license-on-stack-overflow ) I cannot use your wonderful class w/o you granting me it licensed under the BSD License or similar. Pleaseee!!! I've already based a library and app on it and want to distribute it. – Theodore R. Smith Jan 18 '12 at 18:44
  • @TheodoreR.Smith That's really just a basic example. Feel free to use it however you want, without any licensing restrictions (that applies to anyone else too). Good luck with your app :). – svens Jan 18 '12 at 19:52
  • @svens THANK YOU for posting this to the public domain! – Theodore R. Smith Jan 18 '12 at 23:38
9

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.

Jonah
  • 9,991
  • 5
  • 45
  • 79
  • While it's nice that you've posted a resource, it'd be better if you provided some more detail that would be relevant to people who hit this question via Google, etc. Or, put more simply: a link is not an answer. – Brian Driscoll Mar 16 '11 at 00:02
  • @Brian: okay, answer add. I was a bit hesitant before because I wasn't (and still am not) 100% sure that it's what he's asking. – Jonah Mar 16 '11 at 01:43