Improve This PHP Bitfield Class for Settings/Permissions

Improve this PHP bitfield class for settings/permissions?

Others have helped with further explaining the bit masking bit of this, so I'll concentrate on

"I do like the idea of making it more
extensible/generic so different
classes can extend this and use it for
different sections, i'm just not sure
how to do it yet"

from your comment on @Charles' post.

As Charles rightly said, you can re-use the functionality of your Bitmask class by extracting the functionality into an abstract class, and putting the actual "settings" (in this case permissions) into derived concrete classes.

For example:

<?php

abstract class BitField {

private $value;

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

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

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

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

public function clear($n) {
$this->set($n, false);
}
}

class UserPermissions_BitField extends BitField
{
const PERM_READ = 0;
const PERM_WRITE = 1;
const PERM_ADMIN = 2;
const PERM_ADMIN2 = 3;
const PERM_ADMIN3 = 4;
}

class UserPrivacySettings_BitField extends BitField
{
const PRIVACY_TOTAL = 0;
const PRIVACY_EMAIL = 1;
const PRIVACY_NAME = 2;
const PRIVACY_ADDRESS = 3;
const PRIVACY_PHONE = 4;
}

And then usage simply becomes:

<?php
$user_permissions = 0; //This value will come from MySQL or Sessions
$bf = new UserPermissions_BitField($user_permissions);

// turn these permission to on/true
$bf->set($bf::PERM_READ);
$bf->set($bf::PERM_WRITE);
$bf->set($bf::PERM_ADMIN);
$bf->set($bf::PERM_ADMIN2);
$bf->set($bf::PERM_ADMIN3);

And to set privacy settings, you just instantiate a new UserPrivacySettings_BitField object and use that instead.

This way, you can create as many different sets of BitField objects as your application requires simply by defining a set of constants that represent your options.

I hope this is of some use to you, but if not, perhaps it will be of some use to someone else who reads this.

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.

Implement bitmask or relational ACL in PHP

Bitmasks

The original problem with bitmasks is that they go against the conventions of data modelling (expressed in another answer here, with further reading here and here). A 4-byte signed integer might only contain 31 different values (represented in integer as 2 147 483 648) and calculation is hard with them. There have been various questions on Stack Overflow before discussing this topic, which I used to get a sense of implementation of how bistmasks would work.

Testing also revealed that working with bitmasks is hard. It needs a sense to understand the bitwise operators, and migration becomes basically impossible. (By impossible I mean that, at first, implementing bitmasks seemed a good thing to do, but after all it turned out it requires too much expends compared to the benefit it could give in return.) Take a basic portal-like website... I mean no. Take Stack Overflow as an example, and how much unique privileges it has. I have actually tried to make a count but lost myself in complexity, but the amount we would need is way close, if not already over the said barrier of 31 unique values. There is a very likely chance that an update on the project which alters bitmask meanings result in a long need of recalculation, and it is a lot more error prone if confronted by database errors.

While I don't have the exact, pinpoint timing figures, using bitmasks felt slower than ACL. Comparing database sizes, memory and storage footprint is bigger and we have less chance to exploit the relational database and indexing capabilities. For user permissions on a website, bitmasks are a no-go, if we have other methods we might use.

There are various systems where bitmasks do works, namely MaNGOS (from which I originally came up with the idea), where item_template and various other template tables define flags which makes use of bitmasks. But the bottom line is that in these tables, the values are unlikely to ever change, the calculation is read-only, contrary to websites where users arbitrarily obtain and lose privileges.

Example code

define('U_NIL', 0);
define('U_EXEC', 1);
define('U_WRIT', 2);
define('U_READ', 4);

$our_perm = 7;
$req_perm = U_EXEC | U_WRIT | U_READ;
var_dump( ($our_perm & $req_perm) == $req_perm ); # Will be bool(true)

$our_perm = 3;
var_dump( ... The same thing ...); # Will be bool(false)

$our_perm = U_READ;
$req_perm = U_READ | U_WRIT;
var_dumo(...); # Will be bool(false)

$our_perm = U_READ | U_WRIT | U_EXEC;
$req_perm = U_READ;
var_dump(...); # Will be bool(true)

Etcetera.

I will spare you the lines of code because various other questions describe the method aptly, in a way I will never be able to describe it. Bitmasks seem to be nice, bitmasks seem to be exotic, but there is no point letting them settle in production.

ACL

The other option which was described in the original question was to exploit the relational database and the SQL language to set up the permissions in the database. We will need to create two more tables into our database:

CREATE TABLE `perm_relation` (
`row_id` int(10) NOT NULL AUTO_INCREMENT,
`user_id` int(10) NOT NULL,
`perm_id` int(10) NOT NULL,
PRIMARY KEY (`row_id`),
UNIQUE `permission` (`user_id`, `perm_id`)
) ENGINE=InnoDB;

CREATE TABLE `permission` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
PRIMARY KEY (`row_id`)
) ENGINE=InnoDB;

perm_relation.perm_id will be the foreign key pointing to permission.id, and perm_relation.user_id will be our relation to users.id. After this, it's only a matter of how we put together our programming logic.

SELECT row_id FROM perm_relation
WHERE perm_id = (SELECT id FROM permission WHERE name = "U_SUPERADMIN")
AND user_id = CURRENT_USER_ID;

Using this method we bind towards greater compatibility, quicker execution and easier migration. It takes only a neck of time to add new permissions, both as defining new ones and grating some to arbitrary users. Deletion is just as easy and it takes only a small SQL query to optimize our tables to remove orphan entires (for example: users having permissions given to them without related permission defined in the system).

Because we are implementing this system into a PHP environment, we can assume that there will be some sort of administrative page with many features relying on this permission list. An example of listing users filtered by the permission they have would be one example. In this context, ACL is a lot better than bitmasks, because we can take further exploits of JOIN statements... and even beyond.

The conclusion is that bitmasks and the relational pattern has different purposes. Bitmasks are too much and very bulk for a user permission system, while relational tables would be an overkill in the MaNGOS example mentioned above.

Bitwise operations in PHP?

You could use it for bitmasks to encode combinations of things. Basically, it works by giving each bit a meaning, so if you have 00000000, each bit represents something, in addition to being a single decimal number as well. Let's say I have some preferences for users I want to store, but my database is very limited in terms of storage. I could simply store the decimal number and derive from this, which preferences are selected, e.g. 9 is 2^3 + 2^0 is 00001001, so the user has preference 1 and preference 4.

 00000000 Meaning       Bin Dec    | Examples
│││││││└ Preference 1 2^0 1 | Pref 1+2 is Dec 3 is 00000011
││││││└─ Preference 2 2^1 2 | Pref 1+8 is Dec 129 is 10000001
│││││└── Preference 3 2^2 4 | Pref 3,4+6 is Dec 44 is 00101100
││││└─── Preference 4 2^3 8 | all Prefs is Dec 255 is 11111111
│││└──── Preference 5 2^4 16 |
││└───── Preference 6 2^5 32 | etc ...
│└────── Preference 7 2^6 64 |
└─────── Preference 8 2^7 128 |

Further reading

  • http://www.weberdev.com/get_example-3809.html
  • http://stu.mp/2004/06/a-quick-bitmask-howto-for-programmers.html
  • Why should I use bitwise/bitmask in PHP?
  • http://en.wikipedia.org/wiki/Mask_%28computing%29

What to do when you have much permissions with a binary system?

1) Use hex to define the constants - it's much easier to type.

2) Break up the separate permissions into different user roles.

e.g.

const BLOG_POST      = 0x01;
const BLOG_DELETE = 0x02;
const BLOG_UPDATE = 0x04;
const BLOG_READ = 0x08;
const BLOG_SUBSCRIBE = 0x10;

const ADMIN_IS_ADMIN = 0x01;
const ADMIN_ADD_PRODUCT = 0x02;
const ADMIN_DELETE_PRODUCT = 0x04;
const ADMIN_ADD_BLOG = 0x08;

Even when you get up to large numbers, they're still easy to type

const ADMIN_SOME_PERMISSION   = 0x1000;
const ADMIN_SOME_PERMISSION2 = 0x2000;

Why should I use bitwise/bitmask in PHP?

Why not just do this...

define('PERMISSION_DENIED', 0);
define('PERMISSION_READ', 1);
define('PERMISSION_ADD', 2);
define('PERMISSION_UPDATE', 4);
define('PERMISSION_DELETE', 8);

//run function
// this value would be pulled from a user's setting mysql table
$_ARR_permission = 5;

if($_ARR_permission & PERMISSION_READ) {
echo 'Access granted.';
}else {
echo 'Access denied.';
}

You can also create lots of arbitrary combinations of permissions if you use bits...

$read_only = PERMISSION_READ;
$read_delete = PERMISSION_READ | PERMISSION_DELETE;
$full_rights = PERMISSION_DENIED | PERMISSION_READ | PERMISSION_ADD | PERMISSION_UPDATE | PERMISSION_DELETE;

//manipulating permissions is easy...
$myrights = PERMISSION_READ;
$myrights |= PERMISSION_UPDATE; // add Update permission to my rights

What kind of flags/permissions implementations should be?

Don't use a single flag field for this, use a table with a separate row for each flag.

CREATE TABLE flags (
thing_id INT,
flag VARCHAR(32),
PRIMARY KEY (thing_id, flag),
FOREIGN KEY (thing_id) REFERENCES things (id),
INDEX (flag)
);

Rows will look like:

thing_id flag
1 active
1 closed
2 closed
3 deleted
3 active

Then to find all the things with a particular flag, you join the table:

SELECT t.* 
FROM things AS t
JOIN flags as f ON f.thing_id = t.id
WHERE f.flag = 'closed';

To get all of a thing's flags, use GROUP_CONCAT

SELECT t.*, GROUP_CONCAT(f.flag) AS flags
FROM things AS t
JOIN flags as f ON f.thing_id = t.id
WHERE t.id = :thing_id

If you want to prevent creating flags that shouldn't exist, you could make the flag column a foreign key into a flag_names table.



Related Topics



Leave a reply



Submit