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个回答

5

我看到PHP中最常见的枚举解决方案是创建一个通用的枚举类并进行扩展。您可以查看这里

更新:或者,我在phpclasses.org上找到了这个


1
虽然实现很流畅,可能可以胜任工作,但这样做的缺点是IDE可能不知道如何自动填充枚举。我无法检查来自phpclasses.org的枚举,因为它要求我注册。 - Henrik Paul

4

现在你可以使用SplEnum类来本地构建它。根据官方文档。

SplEnum使PHP能够模拟和创建枚举对象。

<?php
class Month extends SplEnum {
    const __default = self::January;

    const January = 1;
    const February = 2;
    const March = 3;
    const April = 4;
    const May = 5;
    const June = 6;
    const July = 7;
    const August = 8;
    const September = 9;
    const October = 10;
    const November = 11;
    const December = 12;
}

echo new Month(Month::June) . PHP_EOL;

try {
    new Month(13);
} catch (UnexpectedValueException $uve) {
    echo $uve->getMessage() . PHP_EOL;
}
?>

请注意,这是一个必须安装的扩展程序,但是它并不是默认提供的。该扩展属于 PHP 官网上描述的 特殊类型 的一部分。以上示例取自 PHP 网站。

4

我知道这是一个旧的线程,然而我看到的所有解决方法中几乎都不像枚举,因为几乎所有的解决方法都要求你手动给枚举项分配值,或者需要你传递一个枚举键的数组给函数。因此,我为此创建了自己的解决方案。

使用我的解决方案创建枚举类很简单,只需扩展下面的Enum类,创建一堆静态变量(无需初始化),并在您的枚举类定义下方调用yourEnumClass::init()即可。

edit: This only works in php >= 5.3, but it can probably be modified to work in older versions as well

/**
 * A base class for enums. 
 * 
 * This class can be used as a base class for enums. 
 * It can be used to create regular enums (incremental indices), but it can also be used to create binary flag values.
 * To create an enum class you can simply extend this class, and make a call to <yourEnumClass>::init() before you use the enum.
 * Preferably this call is made directly after the class declaration. 
 * Example usages:
 * DaysOfTheWeek.class.php
 * abstract class DaysOfTheWeek extends Enum{
 *      static $MONDAY = 1;
 *      static $TUESDAY;
 *      static $WEDNESDAY;
 *      static $THURSDAY;
 *      static $FRIDAY;
 *      static $SATURDAY;
 *      static $SUNDAY;
 * }
 * DaysOfTheWeek::init();
 * 
 * example.php
 * require_once("DaysOfTheWeek.class.php");
 * $today = date('N');
 * if ($today == DaysOfTheWeek::$SUNDAY || $today == DaysOfTheWeek::$SATURDAY)
 *      echo "It's weekend!";
 * 
 * Flags.class.php
 * abstract class Flags extends Enum{
 *      static $FLAG_1;
 *      static $FLAG_2;
 *      static $FLAG_3;
 * }
 * Flags::init(Enum::$BINARY_FLAG);
 * 
 * example2.php
 * require_once("Flags.class.php");
 * $flags = Flags::$FLAG_1 | Flags::$FLAG_2;
 * if ($flags & Flags::$FLAG_1)
 *      echo "Flag_1 is set";
 * 
 * @author Tiddo Langerak
 */
abstract class Enum{

    static $BINARY_FLAG = 1;
    /**
     * This function must be called to initialize the enumeration!
     * 
     * @param bool $flags If the USE_BINARY flag is provided, the enum values will be binary flag values. Default: no flags set.
     */ 
    public static function init($flags = 0){
        //First, we want to get a list of all static properties of the enum class. We'll use the ReflectionClass for this.
        $enum = get_called_class();
        $ref = new ReflectionClass($enum);
        $items = $ref->getStaticProperties();
        //Now we can start assigning values to the items. 
        if ($flags & self::$BINARY_FLAG){
            //If we want binary flag values, our first value should be 1.
            $value = 1;
            //Now we can set the values for all items.
            foreach ($items as $key=>$item){
                if (!isset($item)){                 
                    //If no value is set manually, we should set it.
                    $enum::$$key = $value;
                    //And we need to calculate the new value
                    $value *= 2;
                } else {
                    //If there was already a value set, we will continue starting from that value, but only if that was a valid binary flag value.
                    //Otherwise, we will just skip this item.
                    if ($key != 0 && ($key & ($key - 1) == 0))
                        $value = 2 * $item;
                }
            }
        } else {
            //If we want to use regular indices, we'll start with index 0.
            $value = 0;
            //Now we can set the values for all items.
            foreach ($items as $key=>$item){
                if (!isset($item)){
                    //If no value is set manually, we should set it, and increment the value for the next item.
                    $enum::$$key = $value;
                    $value++;
                } else {
                    //If a value was already set, we'll continue from that value.
                    $value = $item+1;
                }
            }
        }
    }
}


4
class DayOfWeek {
    static $values = array(
        self::MONDAY,
        self::TUESDAY,
        // ...
    );

    const MONDAY  = 0;
    const TUESDAY = 1;
    // ...
}

$today = DayOfWeek::MONDAY;

// If you want to check if a value is valid
assert( in_array( $today, DayOfWeek::$values ) );

不要使用反射。这会使您的代码难以理解和跟踪,容易导致静态分析工具(例如内置于IDE中的工具)失效。


3

接受的答案是最好的方式,也是我出于简单起见正在使用的方式。枚举提供了大多数优点(可读性、速度等)。 然而,有一个概念是缺少的:类型安全。 在大多数语言中,枚举也用于限制允许的值。 下面是一个示例,展示了如何使用私有构造函数、静态实例化方法和类型检查来获得类型安全:

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

 private $intVal;
 private function __construct($intVal){
   $this->intVal = $intVal;
 }

 //static instantiation methods
 public static function MONDAY(){
   return new self(self::Monday);
 }
 //etc.
}

//function using type checking
function printDayOfWeek(DaysOfWeek $d){ //compiler can now use type checking
  // to something with $d...
}

//calling the function is safe!
printDayOfWeek(DaysOfWeek::MONDAY());

我们甚至可以更进一步:在DaysOfWeek类中使用常量可能会导致误用,例如:有人可能错误地这样使用它:
printDayOfWeek(DaysOfWeek::Monday); //triggers a compiler error.

错误的写法是调用整数常量。我们可以使用私有静态变量来替代常量以防止这种情况发生:

class DaysOfWeeks{

  private static $monday = 1;
  //etc.

  private $intVal;
  //private constructor
  private function __construct($intVal){
    $this->intVal = $intVal;
  }

  //public instantiation methods
  public static function MONDAY(){
    return new self(self::$monday);
  }
  //etc.


  //convert an instance to its integer value
  public function intVal(){
    return $this->intVal;
  }

}

当然,访问整数常量是不可能的(这实际上就是目的)。intVal方法允许将DaysOfWeek对象转换为其整数表示。

请注意,我们甚至可以通过在实例化方法中实现缓存机制来进一步优化,在枚举广泛使用的情况下可以节省内存...

希望这能有所帮助。


2

在@Brian Cline的答案基础上,我想发表一下我的意见。

<?php 
/**
 * A class that simulates Enums behaviour
 * <code>
 * class Season extends Enum{
 *    const Spring  = 0;
 *    const Summer = 1;
 *    const Autumn = 2;
 *    const Winter = 3;
 * }
 * 
 * $currentSeason = new Season(Season::Spring);
 * $nextYearSeason = new Season(Season::Spring);
 * $winter = new Season(Season::Winter);
 * $whatever = new Season(-1);               // Throws InvalidArgumentException
 * echo $currentSeason.is(Season::Spring);   // True
 * echo $currentSeason.getName();            // 'Spring'
 * echo $currentSeason.is($nextYearSeason);  // True
 * echo $currentSeason.is(Season::Winter);   // False
 * echo $currentSeason.is(Season::Spring);   // True
 * echo $currentSeason.is($winter);          // False
 * </code>
 * 
 * Class Enum
 * 
 * PHP Version 5.5
 */
abstract class Enum
{
    /**
     * Will contain all the constants of every enum that gets created to 
     * avoid expensive ReflectionClass usage
     * @var array
     */
    private static $_constCacheArray = [];
    /**
     * The value that separates this instance from the rest of the same class
     * @var mixed
     */
    private $_value;
    /**
     * The label of the Enum instance. Will take the string name of the 
     * constant provided, used for logging and human readable messages
     * @var string
     */
    private $_name;
    /**
     * Creates an enum instance, while makes sure that the value given to the 
     * enum is a valid one
     * 
     * @param mixed $value The value of the current
     * 
     * @throws \InvalidArgumentException
     */
    public final function __construct($value)
    {
        $constants = self::_getConstants();
        if (count($constants) !== count(array_unique($constants))) {
            throw new \InvalidArgumentException('Enums cannot contain duplicate constant values');
        }
        if ($name = array_search($value, $constants)) {
            $this->_value = $value;
            $this->_name = $name;
        } else {
            throw new \InvalidArgumentException('Invalid enum value provided');
        }
    }
    /**
     * Returns the constant name of the current enum instance
     * 
     * @return string
     */
    public function getName()
    {
        return $this->_name;
    }
    /**
     * Returns the value of the current enum instance
     * 
     * @return mixed
     */
    public function getValue()
    {
        return $this->_value;
    }
    /**
     * Checks whether this enum instance matches with the provided one.
     * This function should be used to compare Enums at all times instead
     * of an identity comparison 
     * <code>
     * // Assuming EnumObject and EnumObject2 both extend the Enum class
     * // and constants with such values are defined
     * $var  = new EnumObject('test'); 
     * $var2 = new EnumObject('test');
     * $var3 = new EnumObject2('test');
     * $var4 = new EnumObject2('test2');
     * echo $var->is($var2);  // true
     * echo $var->is('test'); // true
     * echo $var->is($var3);  // false
     * echo $var3->is($var4); // false
     * </code>
     * 
     * @param mixed|Enum $enum The value we are comparing this enum object against
     *                         If the value is instance of the Enum class makes
     *                         sure they are instances of the same class as well, 
     *                         otherwise just ensures they have the same value
     * 
     * @return bool
     */
    public final function is($enum)
    {
        // If we are comparing enums, just make
        // sure they have the same toString value
        if (is_subclass_of($enum, __CLASS__)) {
            return get_class($this) === get_class($enum) 
                    && $this->getValue() === $enum->getValue();
        } else {
            // Otherwise assume $enum is the value we are comparing against
            // and do an exact comparison
            return $this->getValue() === $enum;   
        }
    }

    /**
     * Returns the constants that are set for the current Enum instance
     * 
     * @return array
     */
    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];
    }
}

由于某些原因,我无法调用这些函数。它告诉我这样的函数未被声明。我做错了什么?[基本枚举类位于另一个文件中,我正在使用include('enums.php');]。由于某种原因,它无法看到在子类中声明的枚举函数... - Andrew_STOP_RU_WAR_IN_UA
还有...如何从字符串设置它?类似于$currentSeason.set("Spring"); - Andrew_STOP_RU_WAR_IN_UA

2

这里其他答案缺少的一个方面是如何使用枚举和类型提示。

如果您将枚举定义为抽象类中的一组常量,例如:

abstract class ShirtSize {
    public const SMALL = 1;
    public const MEDIUM = 2;
    public const LARGE = 3;
}

如果一个类不能被实例化,那么你就无法在函数参数中使用类型提示 —— 一方面是因为它不能实例化,另一方面是因为ShirtSize::SMALL的类型为int而不是ShirtSize

这就是为什么原生的枚举类型在PHP中会比我们设计的任何东西都更好。但是,我们可以通过保持一个私有属性来近似实现枚举类型,该属性表示枚举类型的值,并将其初始化限制为我们预定义的常量。为了防止任意实例化枚举类型(同时避免白名单类型检查的开销),我们将构造函数设为私有。

class ShirtSize {
    private $size;
    private function __construct ($size) {
        $this->size = $size;
    }
    public function equals (ShirtSize $s) {
        return $this->size === $s->size;
    }
    public static function SMALL () { return new self(1); }
    public static function MEDIUM () { return new self(2); }
    public static function LARGE () { return new self(3); }
}

然后,我们可以像这样使用ShirtSize

function sizeIsAvailable ($productId, ShirtSize $size) {
    // business magic
}
if(sizeIsAvailable($_GET["id"], ShirtSize::LARGE())) {
    echo "Available";
} else {
    echo "Out of stock.";
}
$s2 = ShirtSize::SMALL();
$s3 = ShirtSize::MEDIUM();
echo $s2->equals($s3) ? "SMALL == MEDIUM" : "SMALL != MEDIUM";

从用户的角度来看,最大的区别在于您必须在常量名称上附加()

然而,一个缺点是当==返回true时,比较对象相等性的===将返回false。因此,最好提供一个equals方法,这样用户不必记住使用==而不是===来比较两个枚举值。

编辑:一些现有答案非常相似,特别是:https://dev59.com/pnVC5IYBdhLWcg3wjx5d#25526473


2
这是我对“动态”枚举的看法...这样我就可以使用变量调用它,例如从表单中调用。
请看下面代码块下面的更新版本...
$value = "concert";
$Enumvalue = EnumCategory::enum($value);
//$EnumValue = 1

class EnumCategory{
    const concert = 1;
    const festival = 2;
    const sport = 3;
    const nightlife = 4;
    const theatre = 5;
    const musical = 6;
    const cinema = 7;
    const charity = 8;
    const museum = 9;
    const other = 10;

    public function enum($string){
        return constant('EnumCategory::'.$string);
    }
}

更新:更好的方法...

class EnumCategory {

    static $concert = 1;
    static $festival = 2;
    static $sport = 3;
    static $nightlife = 4;
    static $theatre = 5;
    static $musical = 6;
    static $cinema = 7;
    static $charity = 8;
    static $museum = 9;
    static $other = 10;

}

使用

EnumCategory::${$category};

6
问题在于:EnumCategory::$sport = 9;。欢迎来到体育博物馆。const是更好的处理方式。 - Dan Lugg

2

指出的解决方案很好。干净、流畅。

然而,如果你想要强类型枚举,可以使用以下方法:

class TestEnum extends Enum
{
    public static $TEST1;
    public static $TEST2;
}
TestEnum::init(); // Automatically initializes enum values

枚举类如下:

class Enum
{
    public static function parse($enum)
    {
        $class = get_called_class();
        $vars = get_class_vars($class);
        if (array_key_exists($enum, $vars)) {
            return $vars[$enum];
        }
        return null;
    }

    public static function init()
    {
        $className = get_called_class();
        $consts = get_class_vars($className);
        foreach ($consts as $constant => $value) {
            if (is_null($className::$$constant)) {
                $constantValue = $constant;
                $constantValueName = $className . '::' . $constant . '_VALUE';
                if (defined($constantValueName)) {
                    $constantValue = constant($constantValueName);
                }
                $className::$$constant = new $className($constantValue);
            }
        }
    }

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

这种方式,枚举值是强类型的,并且 TestEnum::$TEST1 === TestEnum::parse('TEST1') 语句返回 true。

2

这里有一些不错的解决方案!

以下是我的版本。

  • 它是强类型的
  • 它与IDE自动完成配合使用
  • 枚举通过代码和描述定义,其中代码可以是整数、二进制值、短字符串或任何其他你想要的东西。该模式可以轻松扩展以支持其他属性。
  • 它支持值(==)和引用(===)比较,并在switch语句中工作。

我认为主要的缺点是枚举成员必须分别声明和实例化,因为需要包含描述并且PHP无法在静态成员声明时构造对象。我猜想一个解决方法可能是使用反射与解析的文档注释。

抽象枚举如下:

<?php

abstract class AbstractEnum
{
    /** @var array cache of all enum instances by class name and integer value */
    private static $allEnumMembers = array();

    /** @var mixed */
    private $code;

    /** @var string */
    private $description;

    /**
     * Return an enum instance of the concrete type on which this static method is called, assuming an instance
     * exists for the passed in value.  Otherwise an exception is thrown.
     *
     * @param $code
     * @return AbstractEnum
     * @throws Exception
     */
    public static function getByCode($code)
    {
        $concreteMembers = &self::getConcreteMembers();

        if (array_key_exists($code, $concreteMembers)) {
            return $concreteMembers[$code];
        }

        throw new Exception("Value '$code' does not exist for enum '".get_called_class()."'");
    }

    public static function getAllMembers()
    {
        return self::getConcreteMembers();
    }

    /**
     * Create, cache and return an instance of the concrete enum type for the supplied primitive value.
     *
     * @param mixed $code code to uniquely identify this enum
     * @param string $description
     * @throws Exception
     * @return AbstractEnum
     */
    protected static function enum($code, $description)
    {
        $concreteMembers = &self::getConcreteMembers();

        if (array_key_exists($code, $concreteMembers)) {
            throw new Exception("Value '$code' has already been added to enum '".get_called_class()."'");
        }

        $concreteMembers[$code] = $concreteEnumInstance = new static($code, $description);

        return $concreteEnumInstance;
    }

    /**
     * @return AbstractEnum[]
     */
    private static function &getConcreteMembers() {
        $thisClassName = get_called_class();

        if (!array_key_exists($thisClassName, self::$allEnumMembers)) {
            $concreteMembers = array();
            self::$allEnumMembers[$thisClassName] = $concreteMembers;
        }

        return self::$allEnumMembers[$thisClassName];
    }

    private function __construct($code, $description)
    {
        $this->code = $code;
        $this->description = $description;
    }

    public function getCode()
    {
        return $this->code;
    }

    public function getDescription()
    {
        return $this->description;
    }
}

以下是一个具体枚举示例:
<?php

require('AbstractEnum.php');

class EMyEnum extends AbstractEnum
{
    /** @var EMyEnum */
    public static $MY_FIRST_VALUE;
    /** @var EMyEnum */
    public static $MY_SECOND_VALUE;
    /** @var EMyEnum */
    public static $MY_THIRD_VALUE;

    public static function _init()
    {
        self::$MY_FIRST_VALUE = self::enum(1, 'My first value');
        self::$MY_SECOND_VALUE = self::enum(2, 'My second value');
        self::$MY_THIRD_VALUE = self::enum(3, 'My third value');
    }
}

EMyEnum::_init();

这可以像这样使用:

<?php

require('EMyEnum.php');

echo EMyEnum::$MY_FIRST_VALUE->getCode().' : '.EMyEnum::$MY_FIRST_VALUE->getDescription().PHP_EOL.PHP_EOL;

var_dump(EMyEnum::getAllMembers());

echo PHP_EOL.EMyEnum::getByCode(2)->getDescription().PHP_EOL;

并生成以下输出:

1 : My first value

array(3) {  
  [1]=>  
  object(EMyEnum)#1 (2) {  
    ["code":"AbstractEnum":private]=>  
    int(1)  
    ["description":"AbstractEnum":private]=>  
    string(14) "My first value"  
  }  
  [2]=>  
  object(EMyEnum)#2 (2) {  
    ["code":"AbstractEnum":private]=>  
    int(2)  
    ["description":"AbstractEnum":private]=>  
    string(15) "My second value"  
  }  
  [3]=>  
  object(EMyEnum)#3 (2) {  
    ["code":"AbstractEnum":private]=>  
    int(3)  
    ["description":"AbstractEnum":private]=>  
    string(14) "My third value"  
  }  
}

My second value


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