marc-mabe/php-enum

使用原生PHP实现枚举的简单快速方法

v4.7.0 2022-04-19 02:21 UTC

README

Build Status Code Coverage License Latest Stable Total Downloads Monthly Downloads Dependents

这是一个为PHP添加枚举支持的本地PHP实现。它是一个需要扩展才能使用的抽象类。

什么是枚举?

维基百科

在计算机编程中,枚举类型(也称为枚举或enum)是由一组称为元素、成员或枚举器的命名值组成的类型。枚举器名称通常是作为语言中的常量行为的标识符。已声明为枚举类型的变量可以被分配任何枚举器作为值。换句话说,枚举类型具有不同的值,可以进行比较和赋值,但在计算机内存中没有特定的具体表示;编译器和解释器可以任意表示它们。

使用方法

基础

use MabeEnum\Enum;

// define an own enumeration class
class UserStatus extends Enum
{
    const INACTIVE = 'i';
    const ACTIVE   = 'a';
    const DELETED  = 'd';

    // all scalar data types and arrays are supported as enumerator values
    const NIL     = null;
    const BOOLEAN = true;
    const INT     = 1234;
    const STR     = 'string';
    const FLOAT   = 0.123;
    const ARR     = ['this', 'is', ['an', 'array']];

    // Enumerators will be generated from public constants only
    public    const PUBLIC_CONST    = 'public constant';    // this will be an enumerator
    protected const PROTECTED_CONST = 'protected constant'; // this will NOT be an enumerator
    private   const PRIVATE_CONST   = 'private constant';   // this will NOT be an enumerator

    // works since PHP-7.0 - see https://wiki.php.net/rfc/context_sensitive_lexer
    const TRUE      = 'true';
    const FALSE     = 'false';
    const NULL      = 'null';
    const PUBLIC    = 'public';
    const PRIVATE   = 'private';
    const PROTECTED = 'protected';
    const FUNCTION  = 'function';
    const TRAIT     = 'trait';
    const INTERFACE = 'interface';

    // Doesn't work - see https://wiki.php.net/rfc/class_name_scalars
    // const CLASS = 'class';
}

// ways to instantiate an enumerator
$status = UserStatus::get(UserStatus::ACTIVE); // by value or instance
$status = UserStatus::ACTIVE();                // by name as callable
$status = UserStatus::byValue('a');            // by value
$status = UserStatus::byName('ACTIVE');        // by name
$status = UserStatus::byOrdinal(1);            // by ordinal number

// basic methods of an instantiated enumerator
$status->getValue();   // returns the selected constant value
$status->getName();    // returns the selected constant name
$status->getOrdinal(); // returns the ordinal number of the selected constant

// basic methods to list defined enumerators
UserStatus::getEnumerators();  // returns a list of enumerator instances
UserStatus::getValues();       // returns a list of enumerator values
UserStatus::getNames();        // returns a list of enumerator names
UserStatus::getOrdinals();     // returns a list of ordinal numbers
UserStatus::getConstants();    // returns an associative array of enumerator names to enumerator values

// same enumerators (of the same enumeration class) holds the same instance
UserStatus::get(UserStatus::ACTIVE) === UserStatus::ACTIVE()
UserStatus::get(UserStatus::DELETED) != UserStatus::INACTIVE()

// simplified way to compare two enumerators
$status = UserStatus::ACTIVE();
$status->is(UserStatus::ACTIVE);     // true
$status->is(UserStatus::ACTIVE());   // true
$status->is(UserStatus::DELETED);    // false
$status->is(UserStatus::DELETED());  // false

类型提示

use MabeEnum\Enum;

class User
{
    protected $status;

    public function setStatus(UserStatus $status)
    {
        $this->status = $status;
    }

    public function getStatus()
    {
        if (!$this->status) {
            // initialize default
            $this->status = UserStatus::INACTIVE();
        }
        return $this->status;
    }
}

类型提示问题

因为在正常的OOP中,上面的示例允许使用 UserStatus 及其派生类型。

请考虑以下示例

class ExtendedUserStatus extends UserStatus
{
    const EXTENDED = 'extended';
}

$user = new User();
$user->setStatus(ExtendedUserStatus::EXTENDED());

现在setter接收到一个它不知道的状态但允许它。

解决方案1:最终化枚举

final class UserStatus extends Enum
{
    // ...
}

class User
{
    protected $status;

    public function setStatus(UserStatus $status)
    {
        $this->status = $status;
    }
}
  • 这是一个好且明显的方法

  • 结果行为与大多数其他语言的本地枚举实现(如Java)相匹配

但作为这个库模拟枚举,它也有一些缺点

  • 枚举值不能直接使用

    • $user->setStatus(UserStatus::ACTIVE) 失败
    • $user->setStatus(UserStatus::ACTIVE()) 工作
  • 如果枚举是在外部库中定义的,这没有帮助

解决方案2:使用 Enum::get()

class User
{
    public function setStatus($status)
    {
        $this->status = UserStatus::get($status);
    }
}
  • 确保结果枚举器与枚举完全匹配。(不允许继承枚举器)

  • 允许直接使用枚举值

    • $user->setStatus(UserStatus::ACTIVE) 工作
    • $user->setStatus(UserStatus::ACTIVE()) 工作
  • 这也适用于在外部库中定义的枚举

但当然,这个解决方案也有缺点

  • 丢失声明性类型提示

  • 稍微慢一些

EnumSet

一个用于枚举类型的专用Set实现,称为 EnumSet。在 EnumSet 中的所有枚举器必须来自创建集合时指定的单个枚举类型。

枚举集在内部表示为位向量。位向量是整数类型或二进制字符串类型,这取决于枚举类型中定义了多少枚举器。这种表示非常紧凑和高效。批量操作将非常快。根据设计,EnumSet 的枚举器是唯一的,并按其序号排序。

它实现了 IteratorAggregateCountable,可以直接使用 foreach 进行迭代,并使用 count() 进行计数。

EnumSet 有一个可变和不可变接口。可变方法以 setaddremove 开头,而不可变方法以 with 开头。

use MabeEnum\EnumSet;

// create a new EnumSet and initialize with the given enumerators
$enumSet = new EnumSet('UserStatus', [UserStatus::ACTIVE()]);

// modify an EnumSet (mutable interface)

// add enumerators (by value or by instance)
$enumSet->addIterable([UserStatus::INACTIVE, UserStatus::DELETED()]);
// or
$enumSet->add(UserStatus::INACTIVE);
$enumSet->add(UserStatus::DELETED());

// remove enumerators (by value or by instance)
$enumSet->removeIterable([UserStatus::INACTIVE, UserStatus::DELETED()]);
// or
$enumSet->remove(UserStatus::INACTIVE);
$enumSet->remove(UserStatus::DELETED());


// The immutable interface will create a new EnumSet for each modification 

// add enumerators (by value or by instance)
$enumSet = $enumSet->withIterable([UserStatus::INACTIVE, UserStatus::DELETED()]);
// or
$enumSet = $enumSet->with(UserStatus::INACTIVE);
$enumSet = $enumSet->with(UserStatus::DELETED());

// remove enumerators (by value or by instance)
$enumSet->withoutIterable([UserStatus::INACTIVE, UserStatus::DELETED()]);
// or
$enumSet = $enumSet->without(UserStatus::INACTIVE);
$enumSet = $enumSet->without(UserStatus::DELETED());


// Test if an enumerator exists (by value or by instance)
$enumSet->has(UserStatus::INACTIVE); // bool


// count the number of enumerators
$enumSet->count();
count($enumSet);

// test for elements
$enumSet->isEmpty();

// convert to array
$enumSet->getValues();      // List of enumerator values
$enumSet->getEnumerators(); // List of enumerator instances
$enumSet->getNames();       // List of enumerator names
$enumSet->getOrdinals();    // List of ordinal numbers


// iterating over the set
foreach ($enumSet as $ordinal => $enum) {
    gettype($ordinal);  // int (the ordinal number of the enumerator)
    get_class($enum);   // UserStatus (enumerator object)
}


// compare two EnumSets
$enumSet->isEqual($other);    // Check if the EnumSet is the same as other
$enumSet->isSubset($other);   // Check if the EnumSet is a subset of other
$enumSet->isSuperset($other); // Check if the EnumSet is a superset of other


// union, intersect, difference and symmetric difference

// ... the mutable interface will modify the set
$enumSet->setUnion($other);     // Enumerators from both this and other (this | other)
$enumSet->setIntersect($other); // Enumerators common to both this and other (this & other)
$enumSet->setDiff($other);      // Enumerators in this but not in other (this - other)
$enumSet->setSymDiff($other);   // Enumerators in either this and other but not in both (this ^ other)

// ... the immutable interface will produce a new set
$enumSet = $enumSet->withUnion($other);     // Enumerators from both this and other (this | other)
$enumSet = $enumSet->withIntersect($other); // Enumerators common to both this and other (this & other)
$enumSet = $enumSet->withDiff($other);      // Enumerators in this but not in other (this - other)
$enumSet = $enumSet->withSymDiff($other);   // Enumerators in either this and other but not in both (this ^ other)

EnumMap

一个 EnumMap 将相同类型的枚举器映射到分配的数据。

它实现了 ArrayAccessCountableIteratorAggregate,因此可以使用 $enumMap[$key]foreachcount() 像正常数组一样访问、迭代和计数。

use MabeEnum\EnumMap;

// create a new EnumMap
$enumMap = new EnumMap('UserStatus');


// read and write key-value-pairs like an array
$enumMap[UserStatus::INACTIVE] = 'inaktiv';
$enumMap[UserStatus::ACTIVE]   = 'aktiv';
$enumMap[UserStatus::DELETED]  = 'gelöscht';
$enumMap[UserStatus::INACTIVE]; // 'inaktiv';
$enumMap[UserStatus::ACTIVE];   // 'aktiv';
$enumMap[UserStatus::DELETED];  // 'gelöscht';

isset($enumMap[UserStatus::DELETED]); // true
unset($enumMap[UserStatus::DELETED]);
isset($enumMap[UserStatus::DELETED]); // false

// ... no matter if you use enumerator values or enumerator objects
$enumMap[UserStatus::INACTIVE()] = 'inaktiv';
$enumMap[UserStatus::ACTIVE()]   = 'aktiv';
$enumMap[UserStatus::DELETED()]  = 'gelöscht';
$enumMap[UserStatus::INACTIVE()]; // 'inaktiv';
$enumMap[UserStatus::ACTIVE()];   // 'aktiv';
$enumMap[UserStatus::DELETED()];  // 'gelöscht';

isset($enumMap[UserStatus::DELETED()]); // true
unset($enumMap[UserStatus::DELETED()]);
isset($enumMap[UserStatus::DELETED()]); // false


// count number of attached elements
$enumMap->count();
count($enumMap);

// test for elements
$enumMap->isEmpty();

// support for null aware exists check
$enumMap[UserStatus::NULL] = null;
isset($enumMap[UserStatus::NULL]); // false
$enumMap->has(UserStatus::NULL);   // true


// iterating over the map
foreach ($enumMap as $enum => $value) {
    get_class($enum);  // UserStatus (enumerator object)
    gettype($value);   // mixed (the value the enumerators maps to)
}

// get a list of keys (= a list of enumerator objects)
$enumMap->getKeys();

// get a list of values (= a list of values the enumerator maps to)
$enumMap->getValues();

序列化

因为这个枚举实现基于单例模式,在PHP中目前无法在不创建新实例的情况下反序列化单例,所以这个特性不支持,除非进行额外的工作。

由于这是一个经常请求的功能,您可以在枚举定义中添加一个特性。该特性增加了序列化功能,并在是第一个实例的情况下注入反序列化的枚举实例。这减少了单例行为中断,但如果不它是第一个实例,仍可能导致相同的枚举有两个不同的实例。

请谨慎使用!

PS:只要您没有设置其他非序列化值,EnumSetEnumMap 默认可序列化。

使用 EnumSerializableTrait 的示例

use MabeEnum\Enum;
use MabeEnum\EnumSerializableTrait;
use Serializable;

class CardinalDirection extends Enum implements Serializable
{
    use EnumSerializableTrait;

    const NORTH = 'n';
    const EAST  = 'e';
    const WEST  = 'w';
    const SOUTH = 's';
}

$north1 = CardinalDirection::NORTH();
$north2 = unserialize(serialize($north1));

var_dump($north1 === $north2);  // returns FALSE as described above
var_dump($north1->is($north2)); // returns TRUE - this way the two instances are treated equal
var_dump($north2->is($north1)); // returns TRUE - equality works in both directions

泛型和静态代码分析器

在版本 4.3 中,我们增加了对泛型的支持,并增加了更好的类型支持。

  • EnumSet<T of Enum>
  • EnumMap<T of Enum>

泛型类型将由 PHPStanPsalm 检测。

此外,我们开发了一个 PHPStan 扩展,以使枚举访问器方法已知。

为什么不使用 SplEnum

  • SplEnum 不是 PHP 内置的,需要安装 pecl 扩展。
  • 相同的 SplEnum 值的实例不是相同的实例。
  • 不支持 EnumMapEnumSet

变更日志

变更记录在 发布页面 上。

安装

Composer

marc-mabe/php-enum 添加到项目的 composer.json 依赖项中,并运行 php composer.phar install

GIT

git clone git://github.com/marc-mabe/php-enum.git

ZIP / TAR

Github 下载最新版本并解压缩。

版本和发布

本项目遵循 SemVer 规范。

没有 LTS 发布,我们没有基于时间的发布窗口。相反,根据需要发布。

我们至少支持所有维护的 PHP 版本。

错误修复将被回溯到最新的维护次要版本。

关键错误修复和安全相关修复也可以回溯到旧版本。

新 BSD 许可证

此存档中的文件根据新 BSD 许可证发布。您可以在 LICENSE.txt 文件中找到此许可证的副本。