14

很长一段时间以来,我一直试图找出在 PHP 中使用位掩码或位域的最佳方法,用于我的应用程序的不同区域,以获得不同的用户设置和权限。到目前为止,我所走的最远的是 svens 在 Stack Overflow 帖子 Bitmask in PHP for settings 中贡献的一个类?. 我在下面稍微修改了它,将其更改为使用类常量而不是 DEFINE,并确保 get 方法仅传递一个 int。我还有一些示例代码来测试下面的类的功能。

我正在寻找任何建议/代码来进一步改进这个类,以便它可以在我的应用程序中用于设置,并在某些情况下用于用户权限。

mcrumley 在下面的评论中回答了

另外,我有一个关于常量编号的问题。在此类型的其他类和代码示例中,它将以 2 的幂列出的内容。但是,据我所知,即使我将常量编号为 1、2、3、4、5、6,它的工作方式似乎也是一样的而不是 1、2、4、8、16 等。那么有人可以澄清我是否应该改变我的常数吗?


一些想法......我真的很想找到一种方法来扩展这个类,这样它就可以很容易地与其他类一起使用。假设我有一个User班级和一个Messages班级。User和_Messages类将扩展此类并能够将位掩码用于其设置/权限(以及稍后的其他类)。因此,也许应该更改当前的类常量,以便可以传入它们或其他一些选项?我真的宁愿不必在站点/脚本的其他部分定义 (define('PERM_READ', 1);) 并且希望保持它的某种封装,但也很灵活;我对想法持开放态度。我希望它是坚如磐石和灵活的,就像我说的与多个其他类一起用于设置或权限。可能应该使用某种数组?@Svens 来自我上面链接的上一个问题,发表了一条评论,“实现一些自动的 getter/setter 或 ArrayAccess 以获得额外的效果。- svens”你对类似的事情也有什么看法?

如果可能,请包含示例源代码。

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

示例用法...

<?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>';
    }
?>
4

5 回答 5

15

在此类型的其他类和代码示例中,它将以 2 的幂列出的内容,但据我所知,即使我将常量编号为 1,2,3,4,5,6 而不是1,2,4,8,16 等等。那么有人可以澄清一下我是否应该改变我的常数吗?

你不需要,因为代码已经处理好了。这个解释会有点迂回。

位字段被处理为2 的幂的原因是每个 2 的幂都由单个位表示。这些单独的位可以按位或一起形成一个可以传递的整数。在较低级别的语言中,传递一个数字比传递一个结构“更容易”。

让我演示一下这是如何工作的。让我们使用两个的幂来设置一些权限:

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

让我们在 PHP 交互式提示符下检查这些权限的位值:

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

现在让我们创建一个具有 READ 访问权限和 WRITE 访问权限的用户。

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

或者可以读、写、删除但不能编辑的用户:

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

我们可以使用按位与检查权限并确保结果不为零:

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)

(值得注意的PERM_NONE & PERM_NONE0 & 0,它为零。我创建的“无”权限实际上在这里不起作用,并且可以很快被遗忘。)

您的班级正在做一些稍微不同的事情,但最终结果是相同的。它使用位移将“on”位向左移动 X 次,其中 X 是权限数。实际上,这是将 2 提高到权限 value 的幂。一个示范:

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

虽然这些方法实际上是相同的,但我认为简单的 ANDing 和 ORing 比 XORing 和位移更容易阅读。

我正在寻找任何建议/代码来进一步改进这个类,以便它可以在我的应用程序中用于设置,在某些情况下还可以用于用户权限。

我有一个建议,一个警告。

我的建议是使类抽象,而不是在其中定义任何权限。相反,构建从它继承的类并定义它们自己的权限。您不想考虑在不相关的位字段之间共享相同的权限名称,并且在它们前面加上类名是非常明智的。我希望你无论如何都会这样做。

我的警告很简单但很可怕: PHP 不能可靠地表示大于 31 位的整数。 事实上,它在 64 位系统上编译时只能表示 63 位整数。这意味着,如果您将应用程序分发给公众,如果您希望使用内置的数学函数 ,您将被限制为不超过 31 个权限。

GMP 扩展包括可以对任意长度整数起作用的位运算。

另一种选择可能是在 large integers 上使用this answer中的代码,这可以让您将一个巨大的整数表示为一个字符串,尽管对其进行按位运算可能......很有趣。(您可以将其向下转换为 base-2,然后在预期的位置对字符串“1”或“0”进行 substr 检查,但这将是一个巨大的性能拖累。)

于 2011-03-23T22:16:05.053 回答
14

其他人已经帮助进一步解释了这个位掩码位,所以我将专注于

“我确实喜欢让它更具可扩展性/通用性的想法,以便不同的类可以扩展它并将其用于不同的部分,我只是不知道该怎么做”

来自您对@Charles 帖子的评论。

正如查尔斯正确地说的那样,您可以通过将功能提取到抽象类中并将实际的“设置”(在本例中为权限)放入派生的具体类中来重用 Bitmask 类的功能。

例如:

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

然后用法就变成了:

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

要设置隐私设置,您只需实例化一个新的 UserPrivacySettings_BitField 对象并使用它。

这样,您只需定义一组表示您的选项的常量,就可以根据应用程序的需要创建尽可能多的不同 BitField 对象集。

我希望这对你有一些用处,但如果没有,也许它对阅读本文的其他人会有一些用处。

于 2011-03-26T00:48:55.810 回答
7

这是我的建议:

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

}
?>

如您所见,我使用 1、2、4、8 等(2 的幂)来简化计算。如果您将一项权限映射到一位,您将拥有:

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

然后你可以使用逻辑操作,例如你最初有这个:

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

如果要添加写入权限,只需要使用按位或运算符即可:

    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

要删除一位,您必须使用 $value & ~$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

最后,如果要测试是否启用了一位,则必须对要测试的 PERM_XXX 执行 $value 操作:

    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

如果结果不为零,则您有权限,否则您没有权限。

于 2011-03-23T22:06:52.593 回答
4

我在您的课堂上看到的最大错误是您将业务逻辑混合到数据结构中。您的类的目的是将多个布尔值(即真/假)存储在一个整数中。这不必课堂上完成,但很方便。这就是它的目的。

我会在类中删除权限标志并将它们外包到您的业务逻辑类中。

<编辑>

数据结构是处理一件事的实体:数据。数据不会以任何方式解释。例如,堆栈是一种数据结构,您可以将内容放入其中,它将首先为您提供最后一项。重点是:它不关心你在里面放了什么:整数、用户对象、指针、汽车、大象,它只会处理数据的存储和检索。

另一方面,业务逻辑是您定义数据结构如何相互交互的地方。这是定义权限的地方,您声明创建博客文章的人可以编辑它,并且不允许其他人编辑。

这是您的应用程序的两种根本不同的视图,不应混为一谈。您可以将您的权限存储在另一个数据结构中(例如,作为整数数组或权限对象的哈希表- 或任何其他数据结构),并且您可以将其他标志存储在您的 BitField 数据结构中(例如您的布尔偏好用户,例如“希望接收时事通讯”或“电子邮件地址已验证”)。

</编辑>

另一个改进是对这些常量使用十六进制值,这将确保您的第 16 个值仍然可读。(我宁愿推荐在常量声明中使用位移操作符,这样更具可读性,但出于性能原因,当前的 PHP 解释器无法做到这一点。)

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

<编辑>

没有十六进制值的同一个类可读性较差,特别是如果您添加更多标志:

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

</编辑>

我将进一步定义 set() 的参数必须是一位整数,而不是标志号。恶魔的 set() 实现就是我的意思:

$this->value |= $n;
于 2011-03-30T17:19:43.953 回答
0

“我确实喜欢让它更具可扩展性/通用性的想法,以便不同的类可以扩展它并将其用于不同的部分,我只是不知道该怎么做”

不要那样做,原因有很多。没有特定的顺序,简而言之:将功能类与数据对象分开。不要扩展不需要继承的东西。使用属性代替,扩展类通常不需要与位掩码类紧密耦合才能工作。此外,在 PHP 中,您只能从一个类扩展。如果您将其用于如此有限的用途,那么扩展对象已经烧毁了该功能。

因此,您可能喜欢不需要在大脑中进行二进制计算,而是有一个类为您封装了二进制计算,并提供了一个更人性化的界面(至少可以说是名称而不是数字)进行交互。美好的。但仅此而已。您可以通过传递二进制值来传递位掩码。如果您不需要二进制值,则枚举可能就是您正在寻找的(具体检查 FlagsEnum)。

于 2011-04-03T09:58:03.970 回答