Improve this PHP bitfield class for settings/permi

2019-01-13 18:51发布

问题:

I have been trying to figure out the best way to use bitmask or bitfields in PHP for a long time now for different areas of my application for different user settings and permissions. The farthest I have come so far is from a class contributed by svens in the Stack Overflow post Bitmask in PHP for settings?. I have slightly modified it below, changing it to use class constants instead of DEFINE and making sure the get method is passed an int only. I also have some sample code to test the class's functionality below.

I am looking for any suggestions/code to improve this class even more so it can be used in my application for settings and in some cases user permissions.

Answered in the comment below by mcrumley

In addition, I have a question about the numbering of my constants. In other classes and code sample for this type it will have things listed in powers of 2. However, it seems to work the same as far as I can tell even if I number my constants 1,2,3,4,5,6 instead of 1, 2, 4, 8, 16, etc. So can someone also clarify if I should change my constants?


Some ideas... I would really like to figure out a way to extend this class so it is easy to use with other classes. Let's say I have a User class and a Messages class. Both the User and Messages class will extend this class and be able to use the bitmask for their settings/permissions (along with other classes later on). So maybe the current class constants should be changed so they can be passed in or some other option? I really would rather not have to define (define('PERM_READ', 1);) in other parts of the site/script and would like to keep it somewhat encapsulated, but flexible as well; I am open to ideas. I want this to be rock solid and flexible like I said to use with multiple other classes for settings or permissions. Possibly some kind of array should be used? @Svens from my previous question linked above posted a comment with "implement some automagic getters/setters or ArrayAccess for extra awesomness. – svens" What do you think about something like that as well?

Include example source code if possible, please.

<?php

class BitField {

    const PERM_READ = 0;
    const PERM_WRITE = 1;
    const PERM_ADMIN = 2;
    const PERM_ADMIN2 = 3;
    const PERM_ADMIN3 = 4;

    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);
    }
}
?>

Example Usage...

<?php
    $user_permissions = 0; //This value will come from MySQL or Sessions
    $bf = new 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);

    // Turn permission PERM_ADMIN2 to off/false
    $bf->clear($bf::PERM_ADMIN2); // sets $bf::PERM_ADMIN2 bit to false

    // Get the total bit value
    $user_permissions = $bf->getValue();

    echo '<br> Bitmask value = ' .$user_permissions. '<br>Test values on/off based off the bitmask value<br>' ;

    // Check if permission PERM_READ is on/true
    if ($bf->get($bf::PERM_READ)) {
        // can read
        echo 'can read is ON<br>';
    }

    if ($bf->get($bf::PERM_WRITE)) {
        // can write
        echo 'can write is ON<br>';
    }

    if ($bf->get($bf::PERM_ADMIN)) {
        // is admin
        echo 'admin is ON<br>';
    }

    if ($bf->get($bf::PERM_ADMIN2)) {
        // is admin 2
        echo 'admin 2 is ON<br>';
    }

    if ($bf->get($bf::PERM_ADMIN3)) {
        // is admin 3
        echo 'admin 3 is ON<br>';
    }
?>

回答1:

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.



回答2:

In other classes and code sample for this type it will have things listed in powers of 2 however it seems to work the same as far as I can tell even if I number my constants 1,2,3,4,5,6 instead of 1,2,4,8,16 etc. So can someone also clarify if I should change my constants?

You don't need to, because the code is already taking care of that. This explanation is going to be a bit roundabout.

The reason that bit fields are handled as powers of two is that each power of two is represented by a single bit. These individual bits can be bitwise-ORed together into a single integer that can be passed around. In lower-level languages, it's "easier" to pass around a number than, say, a struct.

Let me demonstrate how this works. Let's set up some permissions using the powers of two:

define('PERM_NONE', 0);
define('PERM_READ', 1);
define('PERM_WRITE', 2);
define('PERM_EDIT', 4);
define('PERM_DELETE', 8);
define('PERM_SUPER', 16);

Let's inspect the bit values of these permissions at the PHP interactive prompt:

php > printf('%08b', PERM_SUPER);
00010000
php > printf('%08b', PERM_DELETE);
00001000
php > printf('%08b', PERM_EDIT);
00000100
php > printf('%08b', PERM_WRITE);
00000010
php > printf('%08b', PERM_READ);
00000001
php > printf('%08b', PERM_NONE);
00000000

Now let's create a user that has READ access and WRITE access.

php > printf('%08b', PERM_READ | PERM_WRITE);
00000011

Or a user that can read, write, delete, but not edit:

php > printf('%08b', PERM_READ | PERM_WRITE | PERM_DELETE);
00001011

We can check permission using bitwise-AND and making sure the result is not zero:

php > $permission = PERM_READ | PERM_WRITE | PERM_DELETE;
php > var_dump($permission & PERM_WRITE); // This won't be zero.
int(2)
php > var_dump($permission & PERM_EDIT); // This will be zero.
int(0)

(It's worth noting that PERM_NONE & PERM_NONE is 0 & 0, which is zero. The "none" permission I created doesn't actually work here, and can promptly be forgotten about.)

Your class is doing something slightly different, but the end result is identical. It's using bit shifting to move an "on" bit over to the left X times, where X is the number of the permission. In effect, this is raising 2 to the power of the permission's value. A demonstration:

php > echo BitField::PERM_ADMIN3;
4
php > echo pow(2, BitField::PERM_ADMIN3);
16
php > printf('%08b', pow(2, BitField::PERM_ADMIN3));
00010000
php > echo 1 << BitField::PERM_ADMIN3;
16
php > printf('%08b', 1 << BitField::PERM_ADMIN3);
00010000

While these methods are effectively identical, I'd argue that simple ANDing and ORing is easier to read than the XORing and bit-shifting.

I am looking for any suggestions/code to improve this class even more so it can be used in my app for settings and in some cases user permissions.

I have one suggestion, and one warning.

My suggestion would be making the class abstract and not defining any permissions within it. Instead, build classes that inherit from it and define their own permissions. You don't want to consider sharing the same permission names across unrelated bit fields, and prefixing them with class names is pretty sane. I expect you were going to do this anyway.

My warning is simple but dire: PHP can not reliably represent an integer larger than 31 bits. In fact, it can only represent 63-bit integers when it's compiled on a 64-bit system. This means that, if you are distributing your application to the general public, you will be restricted to no more than 31 permissions if you wish to use the built-in math functions.

The GMP extension includes bitwise operations that can function on arbitrary-length integers.

Another option might be using code from this answer on large integers, which could allow you to represent a huge integer as a string, though doing bitwise operations on that might be ... interesting. (You could down-convert it to base-2, then do a substr check for string "1" or "0" at the expected location, but that's gonna be a huge performance drag.)



回答3:

Here is my proposal:

<?php

class BitField {

    const PERM_READ = 1;
    const PERM_WRITE = 2;
    const PERM_ADMIN = 4;
    const PERM_ADMIN2 = 8;
    const PERM_ADMIN3 = 16;

    private $value;

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

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

    public function get($n) {
                return $this->value & $n;
    }

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

    public function clear($n) {
        $this->value &= ~$n;
    }

}
?>

As you can see, I used 1, 2, 4, 8, etc (powers of 2) to simplify the calculations. If you map one permission to one bit you have:

0 0 0 0 0 0 0 1 = PERM_READ = 1
0 0 0 0 0 0 1 0 = PERM_WRITE = 2
0 0 0 0 0 1 0 0 = PERM_ADMIN = 4
etc...

Then you can use logic operations, for example you have this initially:

    0 0 0 0 0 0 0 1 = PERM_READ = 1

If you want to add permissions to write, you only need to use the bitwise OR operator:

    0 0 0 0 0 0 0 1 = PERM_READ = 1
OR  0 0 0 0 0 0 1 0 = PERM_WRITE = 2
=   0 0 0 0 0 0 1 1 = both bits enabled R & W

To remove one bit you have to use $value & ~$bit, for example remove the write bit:

    0 0 0 0 0 0 1 1 = both bits enabled R & W
AND 1 1 1 1 1 1 0 1 = Bitwise negated PERM_WRITE
=   0 0 0 0 0 0 0 1 = result, only the R bit

Finally, if you want to test if one bit is enabled the operation you have to AND $value against the PERM_XXX you want to test:

    0 0 0 0 0 0 1 1 = both bits enabled R & W
AND 0 0 0 0 0 0 1 0 = Want to test PERM_WRITE
=   0 0 0 0 0 0 1 0 = result

If the result is not zero you have the permission, otherwise you don't.



回答4:

The biggest mistake I see in your class is that you're mixing business logic into a data structure. The purpose of your class is to store multiple boolean values (i.e. true/false) in a single integer. This doesn't have to be done in a class, but it is convenient. And that is its purpose.

I would drop the permission flags in the class and outsource them into your business logic classes.

<EDIT>

A data structure is an entity that handles one thing: data. The data is not interpreted in any way. A stack, fore example, is a data structure that you can put stuff into, that will give you the last item first. And here is the point: It doesn't care, what you put in there: integers, User objects, pointers, cars, elephants, it will just handle the storage and retrieval of the data.

Business logic on the other hand is where you define how your data structures interact with each other. This is where permissions are defined, where you state that a person who created a blog post may edit it, and no one else is allowed to.

These are two fundamentally different views of your application and should not be mixed. You can store your permissions in another data structure (as an array of integers, or a hash table of Permission objects, for example - or any other data structure) and you can store other flags in your BitField data structure (like boolean preferences of your users, like "wants to receive newsletter" or "email address was verified").

</EDIT>

Another improvement is the usage of hex values for these constants, this will ensure that your 16th value is still readable. (I would rather recommend using bit-shift operators in the constant declarations, which is even more readable, but this is not possible with the current PHP interpreter for performance reasons.)

class Permission {
    const READ     = 0x0001;
    const UPDATE   = 0x0002;
    const DELETE   = 0x0004;
    const COMMENT  = 0x0008;
    const GRANT    = 0x0010;
    const UNDELETE = 0x0020;
    const WHATEVER = 0x0040;
}

$permissions = new BitField();
$permissions->set(Permission::READ);
$permissions->set(Permission::WRITE);

<EDIT>

The same class without hexadecimal values is less readable, especially if you add more flags:

class Permission {
    const READ         = 1;
    const UPDATE       = 2;
    const DELETE       = 4;
    const COMMENT      = 8;
    const GRANT        = 16;
    const UNDELETE     = 32;
    const WHATEVER     = 64;
    const PERMISSION8  = 128;
    const PERMISSION9  = 256;
    const PERMISSION10 = 512;
    const PERMISSION11 = 1024;
    const PERMISSION12 = 2048;
    const PERMISSION13 = 4096;
    const PERMISSION14 = 8192;
    const PERMISSION15 = 16384;
    const PERMISSION16 = 32768; # the 16th value I mentioned above. Would
                                # you immediately recognize this value as 2^16?
                                # I wouldn't.
    const PERMISSION17 = 65536;
    const PERMISSION18 = 131072;
    const PERMISSION19 = 262144;
}

</EDIT>

I would further define that the parameter to set() must be a single-bit integer, and not a flag number. The set() implementation by demon is what I mean:

$this->value |= $n;


回答5:

"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"

Don't do that, there are various reasons why. In no specific order and just in short: Separate functional classes from data objects. Don't extend what does not need inheritance. Use a property instead, the extending classes normally do not need to be tightly coupled with the bitmask class to work at all. Additionally in PHP you can only extend from one class. If you make use of that for such a limited use, extending objects already have burned that feature.

So probably you love to not need to do binary calculations in your brain but have a class instead that has encapsulated the binary calculation for you and that offers an interface that is more human (names instead of numbers to say at least) to interact with. Fine. But that's just is it. You can pass along the bitmask by passing the binary value. If you don't need binary values, an enum class instead might be what you're looking for already (check the FlagsEnum in specific).