PHP中的枚举类型

1313
我知道PHP目前还没有本地枚举。但是我已经从Java世界中习惯了它们。我希望使用枚举作为一种提供预定义值的方式,以便IDE的自动完成功能能够理解。
常量可以解决问题,但存在命名空间冲突问题,(或者实际上)因为它们是全局的。数组没有命名空间问题,但它们太模糊了,可以在运行时被覆盖,而IDE很少知道如何在不添加额外的静态分析注释或属性的情况下自动填充它们的键。
你通常会使用哪些解决方案/解决方法? 是否有人记得PHP团队是否对枚举有任何想法或决定?

http://it.toolbox.com/blogs/macsploitation/enums-in-php-a-native-implementation-25228 - pbean
1
我创建了一个绕过函数,将常量枚举为按位或非。之前没注意到你问过这个问题,但我有一个比类变量更好的解决方案,在这里:http://stackoverflow.com/questions/3836385/does-php-have-structs-or-enums - rolling_codes
https://github.com/myclabs/php-enum - Matthieu Napoli
我最近开发了一个简单的PHP Enums库: https://github.com/dnl-blkv/simple-php-enum 在回答这个问题时,它仍处于预发布阶段,但已经完全功能,文档齐全,并发布在Packagist上。如果您正在寻找易于实现类似C/C++的enums的便捷选项,这可能是一个不错的选择。 - dnl-blkv
9
PHP的枚举类型将在版本8.1中原生支持,预计将于2021年11月发布。其语法如下:enum Status { case started; case stopped; case paused; } - Abdul Rahman Kayali
1
随着 PHP 8.1 的发布,原生枚举现在已经成为可能,详见文档 - Niki Romagnoli
40个回答

1644
自 PHP 8.1 开始,支持枚举类型:

https://www.php.net/manual/en/language.types.enumerations.php

enum DaysOfWeek: int
{
    case Sunday = 0;
    case Monday = 1;
    // etc.
}
$today = DaysOfWeek::Sunday;
var_dump($today->value); // 0
var_dump($today->name); // "Sunday"

PHP 8.0及更早版本

根据使用情况,我通常会使用类似以下的简单方法:

abstract class DaysOfWeek
{
    const Sunday = 0;
    const Monday = 1;
    // etc.
}

$today = DaysOfWeek::Sunday;

然而,其他用例可能需要对常量和值进行更多的验证。根据下面关于反射的评论和一些其他注意事项,这里是一个扩展示例,可以更好地满足更广泛的用例范围:
abstract class BasicEnum {
    private static $constCacheArray = NULL;

    private static function getConstants() {
        if (self::$constCacheArray == NULL) {
            self::$constCacheArray = [];
        }
        $calledClass = get_called_class();
        if (!array_key_exists($calledClass, self::$constCacheArray)) {
            $reflect = new ReflectionClass($calledClass);
            self::$constCacheArray[$calledClass] = $reflect->getConstants();
        }
        return self::$constCacheArray[$calledClass];
    }

    public static function isValidName($name, $strict = false) {
        $constants = self::getConstants();

        if ($strict) {
            return array_key_exists($name, $constants);
        }

        $keys = array_map('strtolower', array_keys($constants));
        return in_array(strtolower($name), $keys);
    }

    public static function isValidValue($value, $strict = true) {
        $values = array_values(self::getConstants());
        return in_array($value, $values, $strict);
    }
}

通过创建一个简单的枚举类,继承BasicEnum,你现在可以使用以下方法来进行简单的输入验证:
abstract class DaysOfWeek extends BasicEnum {
    const Sunday = 0;
    const Monday = 1;
    const Tuesday = 2;
    const Wednesday = 3;
    const Thursday = 4;
    const Friday = 5;
    const Saturday = 6;
}

DaysOfWeek::isValidName('Humpday');                  // false
DaysOfWeek::isValidName('Monday');                   // true
DaysOfWeek::isValidName('monday');                   // true
DaysOfWeek::isValidName('monday', $strict = true);   // false
DaysOfWeek::isValidName(0);                          // false

DaysOfWeek::isValidValue(0);                         // true
DaysOfWeek::isValidValue(5);                         // true
DaysOfWeek::isValidValue(7);                         // false
DaysOfWeek::isValidValue('Friday');                  // false

顺便提一下,每当我在至少一个静态/常量类中使用反射,并且数据不会改变(比如在枚举中),我会缓存这些反射调用的结果,因为每次使用新的反射对象最终会对性能产生明显影响(在关联数组中存储多个枚举)。

现在大多数人终于升级到至少 5.3 版本,并且有了 SplEnum,那当然也是一种可行的选择,只要你不介意在整个代码库中实际上存在枚举的实例化。在上面的例子中,BasicEnumDaysOfWeek 都不能被实例化,也不应该被实例化。


72
我也使用这个。您可能还需要考虑将该类声明为“抽象”和“final”,以防止它被实例化或扩展。 - ryeguy
23
你可以将一个类同时声明为abstractfinal吗?我知道在Java中是不允许的。但在PHP中呢? - corsiKa
23
@ryeguy 看起来你不能将它同时设置为“抽象”和“最终”。在这种情况下,我会选择抽象。 - Nicole
52
关于抽象类或终态;我将它们设为终态,并给予一个空的私有构造函数。 - user254875486
25
使用数字0时要小心,以免遇到意外的“假值比较”问题,例如在switch语句中与null等值的情况。我曾有过这样的经历。 - yitznewton
显示剩余21条评论

189

5
以下是关于 splenum 的示例:http://www.dreamincode.net/forums/topic/201638-enum-in-php/ - Nordes
4
我回滚了,我更喜欢能看到链接的方式。这会给我提供上下文信息。 - markus
6
我又回滚了。我不希望你们把链接编辑掉。 - markus
7
使用时请小心。SPL类型是实验性的: "该扩展是实验性的。这个扩展包括其函数名称和任何与之相关的文档的行为可能会在未来的PHP版本中发生变化,且不作通知。使用该扩展需自负风险。" - bzeaman
7
SplEnum 没有与 PHP 捆绑在一起,它需要 SPL_Types 扩展 - Kwadz
显示剩余7条评论

161

从PHP 8.1开始,您可以使用本地枚举

基本语法如下:

enum TransportMode {
  case Bicycle;
  case Car;
  case Ship;
  case Plane;
  case Feet;
}
function travelCost(TransportMode $mode, int $distance): int
{ /* implementation */ } 

$mode = TransportMode::Boat;

$bikeCost = travelCost(TransportMode::Bicycle, 90);
$boatCost = travelCost($mode, 90);

// this one would fail: (Enums are singletons, not scalars)
$failCost = travelCost('Car', 90);

价值观

默认情况下,枚举不受任何标量支持。因此,TransportMode::Bicycle 不是 0,并且您不能在枚举之间使用 >< 进行比较。

但以下内容有效:

$foo = TransportMode::Car;
$bar = TransportMode::Car;
$baz = TransportMode::Bicycle;

$foo === $bar; // true
$bar === $baz; // false

$foo instanceof TransportMode; // true

$foo > $bar || $foo <  $bar; // false either way

备份枚举

您还可以拥有“备份”枚举,其中每个枚举案例都由intstring“支持”。

enum Metal: int {
  case Gold = 1932;
  case Silver = 1049;
  case Lead = 1134;
  case Uranium = 1905;
  case Copper = 894;
}
  • 如果一个情况有后备值,则所有情况都需要有后备值,没有自动生成的值。
  • 请注意,后备值的类型在枚举名称右侧声明。
  • 后备值为只读
  • 标量值需要唯一
  • 值需要是文字或文字表达式。
  • 要读取后备值,您可以访问value属性:Metal::Gold->value

最后,支持的枚举在内部实现了BackedEnum接口,该接口公开了两种方法:

  • from(int|string): self
  • tryFrom(int|string): ?self

它们几乎等效,但重要区别在于第一个方法将在未找到值时引发异常,而第二个方法将简单地返回null

// usage example:

$metal_1 = Metal::tryFrom(1932); // $metal_1 === Metal::Gold;
$metal_2 = Metal::tryFrom(1000); // $metal_2 === null;

$metal_3 = Metal::from(9999); // throws Exception

方法

枚举可以拥有方法,从而实现接口。

interface TravelCapable
{
    public function travelCost(int $distance): int;
    public function requiresFuel(): bool;
}

enum TransportMode: int implements TravelCapable{
  case Bicycle = 10;
  case Car = 1000 ;
  case Ship = 800 ;
  case Plane = 2000;
  case Feet = 5;
  
  public function travelCost(int $distance): int
  {
    return $this->value * $distance;
  }
  
  public function requiresFuel(): bool {
    return match($this) {
        TransportMode::Car, TransportMode::Ship, TransportMode::Plane => true,
      TransportMode::Bicycle, TransportMode::Feet => false
    }
  }
}

$mode = TransportMode::Car;

$carConsumesFuel = $mode->requiresFuel();   // true
$carTravelCost   = $mode->travelCost(800);  // 800000

值列表

纯枚举和支持的枚举都在内部实现了接口UnitEnum,其中包括(静态)方法UnitEnum :: cases(),允许检索定义在枚举中的情况数组:

$modes = TransportMode::cases();

现在$modes的值为:

[
    TransportMode::Bicycle,
    TransportMode::Car,
    TransportMode::Ship,
    TransportMode::Plane
    TransportMode::Feet
]

静态方法

枚举可以实现自己的静态方法,通常用于特殊构造函数。


以上是基础内容。要获取全部信息,请前往相关RFC,直到该功能发布并在PHP文档中公布。


2
到了2022年,这个答案应该被标记为“最佳答案”。所有其他答案似乎都是反模式,没有正确地使用语言特性。 - theking2
3
在这个出色的答案上,还有一个小补充。由于::cases()返回一个可迭代类型,因此可以使用foreach循环。例如,像这样:foreach( TransportMode::cases() as $mode ) { echo $mode->value; } - theking2
对于 UnitEnum::cases() 方法,我该如何将其转换为数组,以便我可以在 Select 输入中列出这些 case? - Pathros
"cases() 函数以声明顺序返回所有已定义的 Cases 的压缩数组。" - parttimeturtle

50

类常量怎么处理?

<?php

class YourClass
{
    const SOME_CONSTANT = 1;

    public function echoConstant()
    {
        echo self::SOME_CONSTANT;
    }
}

echo YourClass::SOME_CONSTANT;

$c = new YourClass;
$c->echoConstant();

3
echoConstant can be replaced with __toString. And then simply echo $c - Justinas

44

我使用带有常量的类:

class Enum {
    const NAME       = 'aaaa';
    const SOME_VALUE = 'bbbb';
}

print Enum::NAME;

41

上面的最佳答案非常棒。然而,如果您以两种不同的方式进行扩展,那么先进行哪种扩展就会导致对函数的调用将创建缓存。所有后续调用将使用该缓存,无论由哪种扩展发起调用...

为了解决这个问题,请将变量和第一个函数替换为:

private static $constCacheArray = null;

private static function getConstants() {
    if (self::$constCacheArray === null) self::$constCacheArray = array();

    $calledClass = get_called_class();
    if (!array_key_exists($calledClass, self::$constCacheArray)) {
        $reflect = new \ReflectionClass($calledClass);
        self::$constCacheArray[$calledClass] = $reflect->getConstants();
    }

    return self::$constCacheArray[$calledClass];
}

2
遇到了这个问题。Brian或有编辑权限的人应该在被接受的答案中进行讨论。我在我的代码中使用了“static ::”方法而不是“self ::”来解决getConstants()函数中的问题,并在子枚举中重新声明了$constCache。 - Sp3igel
使用接口常量可能是在PHP中最好的选择,虽然它可能不太吸引人。 - Anthony Rutledge

32

我已在其他答案上发表评论,所以我觉得我也应该发表自己的看法。归根结底,由于PHP不支持类型枚举,你有两种选择:努力破解类型枚举,或者接受它们极难有效地破解这个事实。

我更喜欢接受这个事实,并使用其他答案中已经使用的const方法:

abstract class Enum
{

    const NONE = null;

    final private function __construct()
    {
        throw new NotSupportedException(); // 
    }

    final private function __clone()
    {
        throw new NotSupportedException();
    }

    final public static function toArray()
    {
        return (new ReflectionClass(static::class))->getConstants();
    }

    final public static function isValid($value)
    {
        return in_array($value, static::toArray());
    }

}

一个示例枚举:

final class ResponseStatusCode extends Enum
{

    const OK                         = 200;
    const CREATED                    = 201;
    const ACCEPTED                   = 202;
    // ...
    const SERVICE_UNAVAILABLE        = 503;
    const GATEWAY_TIME_OUT           = 504;
    const HTTP_VERSION_NOT_SUPPORTED = 505;

}

Enum用作所有其他枚举扩展的基类可以使用帮助方法,例如toArrayisValid等。对我来说,有类型的枚举(以及管理它们的实例)变得太凌乱了。


假设

如果存在一个__getStatic魔术方法(最好再加一个__equals魔术方法),这将以一种多例模式缓解其中很多问题。

以下是假设;虽然现在不起作用,但也许有一天会

final class TestEnum
{

    private static $_values = [
        'FOO' => 1,
        'BAR' => 2,
        'QUX' => 3,
    ];
    private static $_instances = [];

    public static function __getStatic($name)
    {
        if (isset(static::$_values[$name]))
        {
            if (empty(static::$_instances[$name]))
            {
                static::$_instances[$name] = new static($name);
            }
            return static::$_instances[$name];
        }
        throw new Exception(sprintf('Invalid enumeration value, "%s"', $name));
    }

    private $_value;

    public function __construct($name)
    {
        $this->_value = static::$_values[$name];
    }

    public function __equals($object)
    {
        if ($object instanceof static)
        {
            return $object->_value === $this->_value;
        }
        return $object === $this->_value;
    }

}

$foo = TestEnum::$FOO; // object(TestEnum)#1 (1) {
                       //   ["_value":"TestEnum":private]=>
                       //   int(1)
                       // }

$zap = TestEnum::$ZAP; // Uncaught exception 'Exception' with message
                       // 'Invalid enumeration member, "ZAP"'

$qux = TestEnum::$QUX;
TestEnum::$QUX == $qux; // true
'hello world!' == $qux; // false

我真的很喜欢这个答案的简洁性。这是一种你可以稍后回来并快速理解其工作原理的东西,而不会让它看起来像你使用了某种黑客方法。可惜它没有更多的赞。 - Reactgular

26

我使用 interface 而不是 class

interface DaysOfWeek
{
    const Sunday = 0;
    const Monday = 1;
    // etc.
}

var $today = DaysOfWeek::Sunday;

6
class Foo implements DaysOfWeek { } 然后 Foo::Sunday ... 是什么意思?这段代码意味着定义了一个类 Foo,它实现了 DaysOfWeek 接口。然后 Foo::Sunday 表示访问 Foo 类中实现的 DaysOfWeek 接口中的常量 Sunday - Dan Lugg
3
问题的作者希望解决两个问题:命名空间和IDE自动补全。正如排名第一的答案所建议的那样,最简单的方法是使用“类”(或“接口”,这只是个人喜好的问题)。 - Andi T
6
接口用于强制执行类实现的完整性,这超出了接口的范围。 - user3886650
3
在Java中,接口可以用于维护常量。这样你就不必实例化一个类来获取常量值,任何IDE都可以对其进行代码补全。此外,如果你创建了一个实现该接口的类,它将继承所有这些常量 - 有时非常方便。 - Alex
@user3886650 是的,但在PHP中,接口可以有常量。此外,这些接口常量不能被实现类或其子类覆盖。实际上,从PHP的角度来看,这是最好的答案,因为任何可以被覆盖的东西都不像常量应该具有的那样真正起作用。常量应该是恒定不变的,而不是有时候(尽管多态有时可能很有用)。 - Anthony Rutledge
然而,我不会像这里所描述的那样访问接口常量。 - Anthony Rutledge

24

好的,对于像Java中的简单枚举类型在PHP中的实现,我使用以下代码:

class SomeTypeName {
    private static $enum = array(1 => "Read", 2 => "Write");

    public function toOrdinal($name) {
        return array_search($name, self::$enum);
    }

    public function toString($ordinal) {
        return self::$enum[$ordinal];
    }
}

并且要调用它:

SomeTypeName::toOrdinal("Read");
SomeTypeName::toString(1);

但我是 PHP 初学者,语法方面还在努力,所以这可能不是最好的方法。我尝试过一些类常量,使用反射从其值获取常量名称,可能更简洁。


好的答案,其他大多数答案都使用类。但是你不能有嵌套类。 - Benbob
这样做的好处是可以使用foreach迭代值。而缺点是无法捕获非法值。 - Bob Stein
2
虽然IDE中没有自动完成,但会刺激猜测。使用常量将启用自动完成,听起来更好。 - KrekkieD

19
四年后我再次遇到了这个。我的当前方法是使用它,因为它允许在IDE中进行代码完成以及类型安全:

基础类:


abstract class TypedEnum
{
    private static $_instancedValues;

    private $_value;
    private $_name;

    private function __construct($value, $name)
    {
        $this->_value = $value;
        $this->_name = $name;
    }

    private static function _fromGetter($getter, $value)
    {
        $reflectionClass = new ReflectionClass(get_called_class());
        $methods = $reflectionClass->getMethods(ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC);    
        $className = get_called_class();

        foreach($methods as $method)
        {
            if ($method->class === $className)
            {
                $enumItem = $method->invoke(null);

                if ($enumItem instanceof $className && $enumItem->$getter() === $value)
                {
                    return $enumItem;
                }
            }
        }

        throw new OutOfRangeException();
    }

    protected static function _create($value)
    {
        if (self::$_instancedValues === null)
        {
            self::$_instancedValues = array();
        }

        $className = get_called_class();

        if (!isset(self::$_instancedValues[$className]))
        {
            self::$_instancedValues[$className] = array();
        }

        if (!isset(self::$_instancedValues[$className][$value]))
        {
            $debugTrace = debug_backtrace();
            $lastCaller = array_shift($debugTrace);

            while ($lastCaller['class'] !== $className && count($debugTrace) > 0)
            {
                $lastCaller = array_shift($debugTrace);
            }

            self::$_instancedValues[$className][$value] = new static($value, $lastCaller['function']);
        }

        return self::$_instancedValues[$className][$value];
    }

    public static function fromValue($value)
    {
        return self::_fromGetter('getValue', $value);
    }

    public static function fromName($value)
    {
        return self::_fromGetter('getName', $value);
    }

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

    public function getName()
    {
        return $this->_name;
    }
}

示例枚举:

final class DaysOfWeek extends TypedEnum
{
    public static function Sunday() { return self::_create(0); }    
    public static function Monday() { return self::_create(1); }
    public static function Tuesday() { return self::_create(2); }   
    public static function Wednesday() { return self::_create(3); }
    public static function Thursday() { return self::_create(4); }  
    public static function Friday() { return self::_create(5); }
    public static function Saturday() { return self::_create(6); }      
}

例子用法:

function saveEvent(DaysOfWeek $weekDay, $comment)
{
    // store week day numeric value and comment:
    $myDatabase->save('myeventtable', 
       array('weekday_id' => $weekDay->getValue()),
       array('comment' => $comment));
}

// call the function, note: DaysOfWeek::Monday() returns an object of type DaysOfWeek
saveEvent(DaysOfWeek::Monday(), 'some comment');
请注意,同一枚举条目的所有实例都是相同的:
$monday1 = DaysOfWeek::Monday();
$monday2 = DaysOfWeek::Monday();
$monday1 === $monday2; // true

您还可以在switch语句内部使用它:

function getGermanWeekDayName(DaysOfWeek $weekDay)
{
    switch ($weekDay)
    {
        case DaysOfWeek::Monday(): return 'Montag';
        case DaysOfWeek::Tuesday(): return 'Dienstag';
        // ...
}

您还可以通过名称或值创建枚举条目:

$monday = DaysOfWeek::fromValue(2);
$tuesday = DaysOfWeek::fromName('Tuesday');

或者你可以从现有的枚举条目中获取名称(即函数名):

$wednesday = DaysOfWeek::Wednesday()
echo $wednesDay->getName(); // Wednesday

私有构造函数加1。我不会将helper抽象类,只是一个简单的类,私有构造函数和一些const Monday = DaysOfWeek('Monday'); - Kangur
1
我有一个疑问。在mysql中,枚举类型的0被视为空白。有效值始终从1开始。如果扩展类的第一个值/整数为0,是否会导致问题?因为我知道mySql/Maria将存储int值,但是如果传递0,则列字符串值始终为空('')。 https://mariadb.com/kb/en/enum/ https://dev.mysql.com/doc/refman/8.0/en/enum.html - Rick Fisk
这个太啰嗦了,你本可以使用protected const并且使用__callStatic来创建枚举,如果你想要自动补全的话,就需要加上文档注释,但你甚至可以移除const,只使用文档注释。 - Tofandel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接