题 PHP和枚举


我知道PHP没有本机Enumerations。但我已经从Java世界习惯了它们。我希望使用枚举作为一种方式来提供IDE的自动完成功能可以理解的预定义值。

常量可以解决问题,但是存在名称空间冲突问题(或实际上 因为)他们是全球性的。数组没有命名空间问题,但是它们太模糊了,它们可以在运行时被覆盖,IDE很少(从不?)知道如何自动填充其键。

您是否经常使用任何解决方案/解决方法?有谁回忆一下PHP家伙是否对枚举有任何想法或决定?


981
2017-10-31 18:51


起源


it.toolbox.com/blogs/macsploitation/... - pbean
我创建了一个函数,它将常量枚举为按位或不按位。没有注意到你之前问过这个问题,但我有一个比类变量更好的解决方案: stackoverflow.com/questions/3836385/... - NoodleOfDeath
github.com/myclabs/php-enum - Matthieu Napoli
你介意分享一下常量问题吗? “常量可以解决问题,但是存在命名空间冲突问题,(或者实际上是因为它们是全局的)。” - XuDing
github.com/githubjeka/enum - Evgeniy Tkachenko


答案:


根据用例,我通常会使用一些东西 简单 如下:

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

作为旁注,任何时候我至少使用一次反射 在静态/ const类上,数据不会改变 (例如在枚举中),我缓存那些反射调用的结果,因为每次使用新的反射对象最终会产生明显的性能影响(存储在多个枚举的关联数组中)。

现在大多数人都有 最后 升级到至少5.3,和 SplEnum 是可用的,这当然是一个可行的选择 - 只要你不介意传统的不直观的概念有实际的枚举 实例 整个代码库。在上面的例子中, BasicEnum 和 DaysOfWeek 根本无法实例化,也不应该实例化。


1323
2017-10-31 18:59



我也用它。你也可以考虑上课 abstract 和 final,因此无法实例化或扩展。 - ryeguy
你可以同时上课 abstract 和 final?我知道在Java中这是不允许的。你可以在PHP中做到这一点? - corsiKa
@ryeguy看来你无法做到 都  abstract 和 final。在那种情况下,我会去抽象。 - Nicole
关于抽象或最终;我让它们最终并给它们一个空的私有构造函数 - rael_kid
使用0时要小心,这样就不会遇到任何意想不到的错误比较问题,例如:等同于 null 和朋友在一起 switch 声明。到过那里。 - yitznewton


还有一个原生扩展。该 SplEnum

SplEnum提供了模拟和创建枚举对象的功能   原生在PHP中。

http://www.php.net/manual/en/class.splenum.php


157
2017-10-31 18:57



这是splenum的一个例子: dreamincode.net/forums/topic/201638-enum-in-php - Nordes
我回头了,当我看到链接时,我更喜欢它。它给了我上下文信息。 - markus
@markus,〜native,因为它需要安装自定义扩展 - takeshin
我又回来了。我不希望你们编辑链接。 - markus
小心使用它。 SPL类型是实验性的: “此扩展程序是实验性的。此扩展程序的行为,包括其功能的名称以及此扩展程序的任何其他文档可能会在未来的PHP版本中发生更改,恕不另行通知。此扩展程序的使用应由您自担风险。” - bzeaman


类常量怎么样?

<?php

class YourClass
{
    const SOME_CONSTANT = 1;

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

echo YourClass::SOME_CONSTANT;

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

35
2018-02-03 20:14





上面的答案很棒。但是,如果你 extend 它以两种不同的方式,然后无论哪个扩展完成,首先导致对函数的调用将创建缓存。然后,所有后续呼叫都将使用此缓存,无论呼叫是由哪个分机发起的......

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

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

28
2017-10-31 18:56



有这个问题。 Brian或具有编辑权限的人应该在接受的答案中触摸它。我在我的代码中使用'static ::'方法解决了它,而不是getConstants()函数中的'self ::',并在子枚举中重新声明了$ constCache。 - Sp3igel


我使用了常量类:

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

print Enum::NAME;

25
2017-11-24 14:51



我们不希望user / developper能够更改Enum的值。这就是为什么你必须先放一个const - Nordes


我用 interface 代替 class

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

var $today = DaysOfWeek::Sunday;

24
2017-12-23 20:04



为了防止实例? - thomthom
class Foo implements DaysOfWeek { } 接着 Foo::Sunday ... 什么? - Dan Lugg
问题的作者要求解决两件事:IDE的命名空间和自动完成。作为评价最高的答案,最简单的方法是使用 class (要么 interface,这只是一个偏好的问题)。 - Andi T
接口用于强制类实现完整性,这超出了接口的范围 - user3886650
@ user3886650 Java中可以使用接口来保持常量值。因此,您不必强制实例化类以获取常量值,并且任何IDE都会在其上提供代码完成。此外,如果您创建一个实现该接口的类,它将继承所有这些常量 - 有时非常方便。 - Alex


好吧,对于像php这样简单的java,我使用:

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的初学者,在语法方面苦苦挣扎,所以这可能不是最好的方法。我用类常量实验了一些,使用Reflection从它的值中获取常量名称,可能更整洁。


20
2018-06-11 13:17



很好的答案,其他大多数答案都是使用课程。但是你不能拥有嵌套类。 - Keyo
这样做的好处是能够使用foreach迭代值。并且不利于非法价值。 - Bob Stein
虽然在IDE中没有自动完成,因此会刺激猜测工作。常量将启用自动完成,听起来更好。 - Greggg


我在这里评论了其他一些答案,所以我想我也会权衡。 在一天结束时,由于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

18
2017-08-27 11:52



我真的很喜欢这个答案的简单性。这是你可以稍后回来并快速了解它是如何工作的东西,而不会让你看起来像你做了某种黑客攻击的方法。遗憾的是它没有更多的选票。 - cgTag


四年后,我又遇到了这个。我目前的方法是这样,因为它允许在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

13
2017-08-21 22:45



+1为私有构造函数。我不会帮助抽象类,只是一个简单的类,私有构造函数和一些 const Monday = DaysOfWeek('Monday'); - Kangur


如果您需要使用全局唯一的枚举(即使在比较不同枚举之间的元素时)并且易于使用,请随意使用以下代码。我还添加了一些我觉得有用的方法。您可以在代码最顶部的注释中找到示例。

<?php

/**
 * Class Enum
 * 
 * @author Christopher Fox <christopher.fox@gmx.de>
 *
 * @version 1.0
 *
 * This class provides the function of an enumeration.
 * The values of Enum elements are unique (even between different Enums)
 * as you would expect them to be.
 *
 * Constructing a new Enum:
 * ========================
 *
 * In the following example we construct an enum called "UserState"
 * with the elements "inactive", "active", "banned" and "deleted".
 * 
 * <code>
 * Enum::Create('UserState', 'inactive', 'active', 'banned', 'deleted');
 * </code>
 *
 * Using Enums:
 * ============
 *
 * The following example demonstrates how to compare two Enum elements
 *
 * <code>
 * var_dump(UserState::inactive == UserState::banned); // result: false
 * var_dump(UserState::active == UserState::active); // result: true
 * </code>
 *
 * Special Enum methods:
 * =====================
 *
 * Get the number of elements in an Enum:
 *
 * <code>
 * echo UserState::CountEntries(); // result: 4
 * </code>
 *
 * Get a list with all elements of the Enum:
 *
 * <code>
 * $allUserStates = UserState::GetEntries();
 * </code>
 *
 * Get a name of an element:
 *
 * <code>
 * echo UserState::GetName(UserState::deleted); // result: deleted
 * </code>
 *
 * Get an integer ID for an element (e.g. to store as a value in a database table):
 * This is simply the index of the element (beginning with 1).
 * Note that this ID is only unique for this Enum but now between different Enums.
 *
 * <code>
 * echo UserState::GetDatabaseID(UserState::active); // result: 2
 * </code>
 */
class Enum
{

    /**
     * @var Enum $instance The only instance of Enum (Singleton)
     */
    private static $instance;

    /**
     * @var array $enums    An array of all enums with Enum names as keys
     *          and arrays of element names as values
     */
    private $enums;

    /**
     * Constructs (the only) Enum instance
     */
    private function __construct()
    {
        $this->enums = array();
    }

    /**
     * Constructs a new enum
     *
     * @param string $name The class name for the enum
     * @param mixed $_ A list of strings to use as names for enum entries
     */
    public static function Create($name, $_)
    {
        // Create (the only) Enum instance if this hasn't happened yet
        if (self::$instance===null)
        {
            self::$instance = new Enum();
        }

        // Fetch the arguments of the function
        $args = func_get_args();
        // Exclude the "name" argument from the array of function arguments,
        // so only the enum element names remain in the array
        array_shift($args);
        self::$instance->add($name, $args);
    }

    /**
     * Creates an enumeration if this hasn't happened yet
     * 
     * @param string $name The class name for the enum
     * @param array $fields The names of the enum elements
     */
    private function add($name, $fields)
    {
        if (!array_key_exists($name, $this->enums))
        {
            $this->enums[$name] = array();

            // Generate the code of the class for this enumeration
            $classDeclaration =     "class " . $name . " {\n"
                        . "private static \$name = '" . $name . "';\n"
                        . $this->getClassConstants($name, $fields)
                        . $this->getFunctionGetEntries($name)
                        . $this->getFunctionCountEntries($name)
                        . $this->getFunctionGetDatabaseID()
                        . $this->getFunctionGetName()
                        . "}";

            // Create the class for this enumeration
            eval($classDeclaration);
        }
    }

    /**
     * Returns the code of the class constants
     * for an enumeration. These are the representations
     * of the elements.
     * 
     * @param string $name The class name for the enum
     * @param array $fields The names of the enum elements
     *
     * @return string The code of the class constants
     */
    private function getClassConstants($name, $fields)
    {
        $constants = '';

        foreach ($fields as $field)
        {
            // Create a unique ID for the Enum element
            // This ID is unique because class and variables
            // names can't contain a semicolon. Therefore we
            // can use the semicolon as a separator here.
            $uniqueID = $name . ";" . $field;
            $constants .=   "const " . $field . " = '". $uniqueID . "';\n";
            // Store the unique ID
            array_push($this->enums[$name], $uniqueID);
        }

        return $constants;
    }

    /**
     * Returns the code of the function "GetEntries()"
     * for an enumeration
     * 
     * @param string $name The class name for the enum
     *
     * @return string The code of the function "GetEntries()"
     */
    private function getFunctionGetEntries($name) 
    {
        $entryList = '';        

        // Put the unique element IDs in single quotes and
        // separate them with commas
        foreach ($this->enums[$name] as $key => $entry)
        {
            if ($key > 0) $entryList .= ',';
            $entryList .= "'" . $entry . "'";
        }

        return  "public static function GetEntries() { \n"
            . " return array(" . $entryList . ");\n"
            . "}\n";
    }

    /**
     * Returns the code of the function "CountEntries()"
     * for an enumeration
     * 
     * @param string $name The class name for the enum
     *
     * @return string The code of the function "CountEntries()"
     */
    private function getFunctionCountEntries($name) 
    {
        // This function will simply return a constant number (e.g. return 5;)
        return  "public static function CountEntries() { \n"
            . " return " . count($this->enums[$name]) . ";\n"
            . "}\n";
    }

    /**
     * Returns the code of the function "GetDatabaseID()"
     * for an enumeration
     * 
     * @return string The code of the function "GetDatabaseID()"
     */
    private function getFunctionGetDatabaseID()
    {
        // Check for the index of this element inside of the array
        // of elements and add +1
        return  "public static function GetDatabaseID(\$entry) { \n"
            . "\$key = array_search(\$entry, self::GetEntries());\n"
            . " return \$key + 1;\n"
            . "}\n";
    }

    /**
     * Returns the code of the function "GetName()"
     * for an enumeration
     *
     * @return string The code of the function "GetName()"
     */
    private function getFunctionGetName()
    {
        // Remove the class name from the unique ID 
        // and return this value (which is the element name)
        return  "public static function GetName(\$entry) { \n"
            . "return substr(\$entry, strlen(self::\$name) + 1 , strlen(\$entry));\n"
            . "}\n";
    }

}


?>

7
2018-04-13 09:55



我很喜欢这个。但是,主要的抱怨之一是IDE能够获取自动完成的值。如果没有IDE的自定义插件,我不确定这是否能够做到这一点。并非它无法完成,它只需要一些工作。 - corsiKa
运用 eval() 只是这样你可以声明新的Enums运行时?伊克。我感觉不到。在定义合适的类之前,如何防止其他类创建错误的Enum类?在运行时之前,Enums不是已知的吗?正如@corsiKa暗示的那样,没有IDE自动完成。我看到的唯一好处是延迟编码。 - Greggg


我也喜欢java中的枚举,因此我以这种方式编写我的枚举,我认为这是Java枚举中最类似的行为,当然,如果有人想要使用java中的更多方法应该在这里写,或者抽象类,但核心思想嵌入在下面的代码中


class FruitsEnum {

    static $APPLE = null;
    static $ORANGE = null;

    private $value = null;

    public static $map;

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

    public static function init () {
        self::$APPLE  = new FruitsEnum("Apple");
        self::$ORANGE = new FruitsEnum("Orange");
        //static map to get object by name - example Enum::get("INIT") - returns Enum::$INIT object;
        self::$map = array (
            "Apple" => self::$APPLE,
            "Orange" => self::$ORANGE
        );
    }

    public static function get($element) {
        if($element == null)
            return null;
        return self::$map[$element];
    }

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

    public function equals(FruitsEnum $element) {
        return $element->getValue() == $this->getValue();
    }

    public function __toString () {
        return $this->value;
    }
}
FruitsEnum::init();

var_dump(FruitsEnum::$APPLE->equals(FruitsEnum::$APPLE)); //true
var_dump(FruitsEnum::$APPLE->equals(FruitsEnum::$ORANGE)); //false
var_dump(FruitsEnum::$APPLE instanceof FruitsEnum); //true
var_dump(FruitsEnum::get("Apple")->equals(FruitsEnum::$APPLE)); //true - enum from string
var_dump(FruitsEnum::get("Apple")->equals(FruitsEnum::get("Orange"))); //false


7
2017-10-31 18:55



我做了几乎相同的事情,虽然有两个小的补充:我隐藏了静态getter背后的静态值。一个原因是,我在视觉上更喜欢 FruitsEnum::Apple() 过度 FruitsEnum::$Apple,但更重要的原因是阻止其他人设置 $APPLE从而打破了整个应用程序的枚举。另一个是简单的私有静态标志 $initialized 确保通话 init() 在第一次调用之后变为无操作(所以没有人可以搞砸它)。 - Martin Ender
我确实喜欢马丁。 .init() 很奇怪,我不介意吸气器方法。 - Sebas