你是如何处理 PHP 代码中的枚举类型 Enum 的?

898 阅读8分钟

文章转发自专业的Laravel开发者社区,原始链接:learnku.com/laravel/t/7…

本文旨在提供一些更好的理解什么是枚举,什么时候使用它们以及如何在php中使用它们.

我们在某些时候使用了常量来定义代码中的一些常数值.他们被用来避免魔法值.用一个象征性的名字代替一些魔法值,我们可以给它一些意义.然后我们在代码中引用这个符号名称.因为我们定义了一次并使用了很多次,所以搜索它并稍后重命名或更改一个值会更容易.

这就是为什么看到类似于下面的代码并不罕见.


<?php
class User {
    const GENDER_MALE = 0;
    const GENDER_FEMALE = 1;
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;
}

以上常量表示了两组属性,GEDNER_* 和 STATUS_*。他们表示一组性别和一组用户状态。每一组都是一个枚举 。枚举是一组元素(也叫做成员)的集合,每一个枚举都定义了一种新类型。这个类型,和它的值一样,可以包含任意属于该枚举的元素。

在上面的例子中,枚举借助于常量,每一个常量的值都是一个成员。注意,这样做的话,我们只能在常量包含的类型中取值。因此,我们在写这些值的时候不会有类型提示,不知道详细的枚举类型。

来看一个简短的例子, 但我们假定例子中有更多的代码

<?php
interface UserFactory {
    public function create(
        string $email,
        int $gender,
        int $status
    ): User;
}
$factory->create(
    $email,
    User::STATUS_ACTIVE,
    User::GENDER_FEMALE
);

第一眼看上去代码很好,但是他只是碰巧正确运行了!因为两个不同的枚举成员实际上是同一个值,调用create方法成功,是因为这最后两个参数被互换了不影响结果。尽管我们检查方法接受的值是否有效,运行界面也不会警告我们,测试也会通过。有人能正确的发现这些bug,但是它也很可能被忽视掉。之后一些情况,比如合并冲突的时候,如果它的值改变了,它可能会引起系统异常。

如果使用标量类型,我们会受限于这种类型,无法辨别这两个值是是不是属于两个不同的枚举。

另一个问题是这个代码描述的的不是很好。想象一下 create 方法没有引用常量。$gender 被别人看作为一个枚举元素将是有多么困难?看这些元素在哪里被定义又有多么困难?我们之后将会阅读那些代码,因此我们应该尽可能是让代码易于阅读以及和通过。

我们可以做得更好吗? Sure! 这个方法就是是使用类实例作为枚举元素,类本身定义了一个新的类型。 直到PHP 7,我们可以安装 SPL类 PECL扩展并且使用SplEnum


<?php
class YesNo extends \SplEnum
{
    const __default =  self::YES;
    const NO = 0;
    const YES = 1;
}
$no = new YesNo(YesNo::NO);
var_dump($no == YesNo::NO); //true
var_dump(new YesNo(YesNo::NO) == YesNo::NO); //true

我们扩展 SplEnum 并且定义用于创建枚举元素的常量。枚举元素是我们手动构造的对象,在这种情况下是常量值本身。 我们可以将整型与对象进行比较,这可能很奇怪。 另外,正如文档所述,这是一个仿真的枚举。 PHP本身并不支持枚举类型,所以我们在这里探讨的所有内容都是仿真的。

我们用这种方法得到了什么? 我们可以输入提示我们的参数,并让PHP引擎在发生错误时提醒我们。 我们还可以在枚举类中包含一些逻辑,并使用switch语句来模拟多态行为。

但也有一些缺点. 例如, 在大多数情况下, 有些你可以用枚举元素而不能用标识检查. 这不是不可能的,我们不得不非常小心. 由于我们手动创建枚举成员, 所以许多成员应该是同一个成员, 但这一点手动很难确定.

利用 SplEnum 我们解决枚举类型问题, 但是当我们用标识检查的时候不得不非常小心. 我们需要一个方法限制可以创建的多个元素, 例如  multiton (multiple singleton objects).

现在我们将看到由 Java Enum 启发并实现 multiton 的两个不同的库.

第一个是 eloquent/enumeration. 它为每个元素创建一个定义类的实例. 请注意, 没有我们的帮助, 枚举的用户仿真永远不能保证一个枚举实例, 因为我们限制它的每一步都有一个方法去避免.

这个库可以让我们用错误的方式去尝试, 例如用反射创建一个实例, 在这一点上我们可以问我们自己是否做了正确的事. 它也可以在代码的评审过程中有所帮助,因为这样的实现可以定义几个应该被遵循的规则. 如果这些规则比较简单很容易发现代码中存在的问题.

让我们看些实例.


<?php
final class YesNo extends \Eloquent\Enumeration\AbstractEnumeration {
    const NO = 0;
    const YES = 1;
}
var_dump(YesNo::YES()->key()); // YES

我们定义了一个继承  \Eloquent\Enumeration\AbstractEnumeration 的新类 YesNo . 接下来我们定义一个定义元素名和创建表现这些元素的对象的库的常量.

还有一些情况我们需要谨记,用 serialize/deserialize 在其中创建自定义对象 .

我们可以在GitHub页面上找到更多的例子和很完善的文档。

我们要展示的第二个库是 zlikavac32/php-enum. 与 eloquent/enumeration不同,这个库面向允许真正的多态行为的抽象类。 所以,我们可以用每个方法都定义一个枚举元素来实现,而不是使用switch的方法。 通过严格的规则来定义枚举,也可以相当可靠地确保每个元素只有一个实例。

这个库面向抽象类,以便将每个成员的许多实例限制为一个。 这个想法是,每个枚举必须被定义为抽象的,并枚举它的元素。 请注意,你可以通过扩展类,然后构造一个元素来滥用,但是如果你这么用了,这些是会在代码审查过程中标红的。

对于抽象类,我们知道我们不会意外地有一个枚举的新元素,因为它需要具体的实现。 通过遵循在enum本身中保持这些具体实现的规则,我们可以很容易地发现滥用。  匿名类 在这里很有用。

库强制抽象枚举类,但不能强制创建有效的元素。 这是这个库的用户的责任。 图书馆照顾其余的。

让我们看一个简单的例子。


<?php
/**
 * @method static YesNo YES
 * @method static YesNo NO
 */
abstract class YesNo extends \Zlikavac32\Enum\Enum
{
    protected static function enumerate(): array
    {
        return [
            'YES', 'NO'
        ];
    }
}
var_dump(YesNo::YES()->name()); // YES

PHPDoc注释定义了返回枚举元素的现有静态方法。 这有助于搜索和重构代码。 接下来,我们将枚举YesNo定义为抽象,并扩展\Zlikavac32\Enum\Enum并定义一个静态方法enumerate。 然后,在enumerate方法中,我们列出将被用来表示它们的元素名称。

刚刚我们提到了多态行为,那么为什么我们会使用它呢? 当我们试图限制同一个枚举元素的多个实例时会发生一件事,那就是我们不能有循环引用。 让我们想象一下,我们想拥有由NORTHSOUTHEASTWEST组成的WorldSide枚举。 我们还想有一个方法opposite():WorldSide,它返回代表相反的元素。 如果我们试图通过构造函数注入相反元素,在某一时刻,我们获得一个循环引用,这意味着,我们需要相同元素的第二个实例。 为了返回一个有效的相反世界,我们不得不用一个代理对象 或者switch语句破解。

随着多态行为,我们能做的就是让我们看到我们可定义我们需要的WorldSide枚举。

<?php
/**
 * @method static WorldSide NORTH
 * @method static WorldSide SOUTH
 * @method static WorldSide EAST
 * @method static WorldSide WEST
 */
abstract class WorldSide extends \Zlikavac32\Enum\Enum
{
    protected static function enumerate(): array
    {
        return [
            'NORTH' => new class extends WorldSide {
                public function opposite(): WorldSide {
                    return WorldSide::SOUTH();
                }
            },
            'SOUTH' => new class extends WorldSide {
                public function opposite(): WorldSide {
                    return WorldSide::NORTH();
                }
            },
            'EAST' => new class extends WorldSide {
                public function opposite(): WorldSide {
                    return WorldSide::WEST();
                }
            },
            'WEST' => new class extends WorldSide {
                public function opposite(): WorldSide {
                    return WorldSide::EAST();
                }
            }
        ];
    }
    abstract public function opposite(): WorldSide;
}
foreach (WorldSide::iterator() as $worldSide) {
    var_dump(sprintf(
        'Opposite of %s is %s', 
        (string) $worldSide, 
        (string) $worldSide->opposite()
    ));
}

enumerate 方法,我们提供了每一个枚举元素的实现。数组是用枚举元素名称来索引的。当手动的创建元素,我们定义我们元素名称作为数据的键。

我们可以用 WorldSide::iterator() 获取枚举元素的顺序迭代器,来定义和遍历他们。 每一个枚举元素都有一个默认的 __toString(): string实现返回元素的名称。

每个枚举元素返回其相反的元素。

回顾一下,常量不是枚举,枚举不是常量。每个枚举定义一个类型。如果我们有一些常数的值对我们很重要,但名字没有,我们应该坚持常数。如果我们有一些常量的价值对我们无关紧要,但是与同一群体中的其他所有人有所不同则是重要的,请使用枚举

枚举为代码提供了更多的上下文,也可以将某些检查委托给引擎本身。如果PHP有一个本地的枚举支持,这将是非常好的。语法更改可以使代码更具可读性。引擎可以为我们执行检查,并执行一些不能从用户区执行的规则。

你如何使用枚举,你对这个主题有什么想法?请在下方评论