PHP and Enumerations

后端 未结 30 1393
有刺的猬
有刺的猬 2020-11-22 13:39

I know that PHP doesn\'t have native Enumerations. But I have become accustomed to them from the Java world. I would love to use enums as a way to give predefined values whi

相关标签:
30条回答
  • 2020-11-22 14:03

    The top answer above is fantastic. However, if you extend it in two different ways, then whichever extension is done first results in a call to the functions will create the cache. This cache will then be used by all subsequent calls, no matter whichever extension the calls are initiated by ...

    To solve this, replace the variable and first function with:

    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];
    }
    
    0 讨论(0)
  • 2020-11-22 14:05

    If you need to use enums that are globally unique (i.e. even when comparing elements between different Enums) and are easy to use, feel free to use the following code. I also added some methods that I find useful. You will find examples in the comments at the very top of the code.

    <?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";
        }
    
    }
    
    
    ?>
    
    0 讨论(0)
  • 2020-11-22 14:05

    I have taken to using the approach below as it gives me the ability to have type safety for function parameters, auto complete in NetBeans and good performance. The one thing I don't like too much is that you have to call [extended class name]::enumerate(); after defining the class.

    abstract class Enum {
    
        private $_value;
    
        protected function __construct($value) {
            $this->_value = $value;
        }
    
        public function __toString() {
            return (string) $this->_value;
        }
    
        public static function enumerate() {
            $class = get_called_class();
            $ref = new ReflectionClass($class);
            $statics = $ref->getStaticProperties();
            foreach ($statics as $name => $value) {
                $ref->setStaticPropertyValue($name, new $class($value));
            }
        }
    }
    
    class DaysOfWeek extends Enum {
        public static $MONDAY = 0;
        public static $SUNDAY = 1;
        // etc.
    }
    DaysOfWeek::enumerate();
    
    function isMonday(DaysOfWeek $d) {
        if ($d == DaysOfWeek::$MONDAY) {
            return true;
        } else {
            return false;
        }
    }
    
    $day = DaysOfWeek::$MONDAY;
    echo (isMonday($day) ? "bummer it's monday" : "Yay! it's not monday");
    
    0 讨论(0)
  • 2020-11-22 14:05

    One of the aspects missing from some of the other answers here is a way to use enums with type hinting.

    If you define your enum as a set of constants in an abstract class, e.g.

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

    then you can't type hint it in a function parameter - for one, because it's not instantiable, but also because the type of ShirtSize::SMALL is int, not ShirtSize.

    That's why native enums in PHP would be so much better than anything we can come up with. However, we can approximate an enum by keeping a private property which represents the value of the enum, and then restricting the initialization of this property to our predefined constants. To prevent the enum from being instantiated arbitrarily (without the overhead of type-checking a whitelist), we make the constructor private.

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

    Then we can use ShirtSize like this:

    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";
    

    This way, the biggest difference from the user's perspective is that you have to tack on a () on the constant's name.

    One downside though is that === (which compares object equality) will return false when == returns true. For that reason, it's best to provide an equals method, so that users don't have to remember to use == and not === to compare two enum values.

    EDIT: A couple of the existing answers are very similar, particularly: https://stackoverflow.com/a/25526473/2407870.

    0 讨论(0)
  • 2020-11-22 14:09

    Depending upon use case, I would normally use something simple like the following:

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

    However, other use cases may require more validation of constants and values. Based on the comments below about reflection, and a few other notes, here's an expanded example which may better serve a much wider range of cases:

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

    By creating a simple enum class that extends BasicEnum, you now have the ability to use methods thusly for simple input validation:

    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
    

    As a side note, any time I use reflection at least once on a static/const class where the data won't change (such as in an enum), I cache the results of those reflection calls, since using fresh reflection objects each time will eventually have a noticeable performance impact (Stored in an assocciative array for multiple enums).

    Now that most people have finally upgraded to at least 5.3, and SplEnum is available, that is certainly a viable option as well--as long as you don't mind the traditionally unintuitive notion of having actual enum instantiations throughout your codebase. In the above example, BasicEnum and DaysOfWeek cannot be instantiated at all, nor should they be.

    0 讨论(0)
  • 2020-11-22 14:09

    I found this library on github and I think it provides a very decent alternative to the answers here.

    PHP Enum implementation inspired from SplEnum

    • You can type-hint: function setAction(Action $action) {
    • You can enrich the enum with methods (e.g. format, parse, …)
    • You can extend the enum to add new values (make your enum final to prevent it)
    • You can get a list of all the possible values (see below)

    Declaration

    <?php
    use MyCLabs\Enum\Enum;
    
    /**
     * Action enum
     */
    class Action extends Enum
    {
        const VIEW = 'view';
        const EDIT = 'edit';
    }
    

    Usage

    <?php
    $action = new Action(Action::VIEW);
    
    // or
    $action = Action::VIEW();
    

    type-hint enum values:

    <?php
    function setAction(Action $action) {
        // ...
    }
    
    0 讨论(0)
提交回复
热议问题