PHP и перечисления - PullRequest
       136

PHP и перечисления

1078 голосов
/ 31 октября 2008

Я знаю, что в PHP нет нативных перечислений. Но я привык к ним из мира Java. Я хотел бы использовать перечисления как способ дать предопределенные значения, которые могли бы понимать функции автозаполнения IDE.

Константы делают свое дело, но есть проблема столкновения пространства имен и (или на самом деле , потому что ) они глобальные. Массивы не имеют проблем с пространством имен, но они слишком расплывчаты, их можно перезаписать во время выполнения, и IDE редко (никогда?) Знают, как автоматически заполнять свои ключи.

Какие решения / обходные пути вы обычно используете? Кто-нибудь помнит, были ли у ребят из PHP какие-то мысли или решения по поводу перечислений?

Ответы [ 37 ]

3 голосов
/ 28 декабря 2011

Я знаю, что это старый поток, однако ни один из обходных путей, которые я видел, действительно не выглядел как перечисления, так как почти все обходные пути требуют, чтобы вы вручную присваивали значения элементам перечисления, или он требует, чтобы вы передавали массив перечислений ключи к функции. Поэтому я создал собственное решение для этого.

Чтобы создать класс enum с использованием моего решения, можно просто расширить этот класс Enum ниже, создать набор статических переменных (не нужно их инициализировать) и вызвать ваш yourEnumClass :: init () чуть ниже определения Ваш enum класс.

edit: Это работает только в php> = 5.3, но, вероятно, может быть изменено для работы в более старых версиях

/**
 * 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;
                }
            }
        }
    }
}
2 голосов
/ 29 ноября 2012

Принятый ответ - это путь, и на самом деле я делаю это для простоты. Предлагаются большинство преимуществ перечисления (удобочитаемые, быстрые и т. Д.). Однако отсутствует одна концепция: безопасность типов. В большинстве языков перечисления также используются для ограничения допустимых значений. Ниже приведен пример того, как безопасность типов также можно получить с помощью частных конструкторов, статических методов создания экземпляров и проверки типов:

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 голосов
/ 21 февраля 2013

Несколько хороших решений здесь!

Вот моя версия.

  • Это сильно напечатано
  • Работает с автозаполнением IDE
  • Перечисления определяются кодом и описанием, где код может быть целым числом, двоичным значением, короткой строкой или, в общем, чем угодно. Шаблон может быть легко расширен для поддержки других свойств.
  • Поддерживает сравнение значений (==) и ссылок (===) и работает в операторах переключения.

Я думаю, что основным недостатком является то, что члены перечисления должны быть отдельно объявлены и созданы из-за описаний и неспособности 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: мое первое значение

массив (3) {
[1] => * +1032 * объект (EMyEnum) # 1 (2) {
[ "Код": "AbstractEnum": частная] =>
INT (1)
[ "Описание": "AbstractEnum": частная] =>
строка (14) «Мое первое значение»
}
[2] => * 1 039 * объект (EMyEnum) # 2 (2) {
[ "Код": "AbstractEnum": частная] =>
Int (2)
[ "Описание": "AbstractEnum": частная] =>
строка (15) «Мое второе значение»
}
[3] =>
объект (EMyEnum) # 3 (2) {
[ "Код": "AbstractEnum": частная] =>
Int (3)
[ "Описание": "AbstractEnum": частная] =>
строка (14) «Мое третье значение»
}
}

Мое второе значение

2 голосов
/ 09 октября 2015

Подойдя к ответу @Brian Cline, я подумал, что могу дать свои 5 центов

<?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];
    }
}
2 голосов
/ 31 марта 2015

Одним из аспектов, отсутствующих в некоторых других ответах, здесь является способ использования перечислений с подсказками типов.

Если вы определяете свое перечисление как набор констант в абстрактном классе, например,

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

Таким образом, самое большое отличие с точки зрения пользователя заключается в том, что вам нужно добавить () к имени константы.

Единственным недостатком является то, что === (который сравнивает равенство объектов) вернет false, когда == вернет true. По этой причине лучше всего предоставить метод equals, чтобы пользователям не приходилось не забывать использовать ==, а не === для сравнения двух значений перечисления.

РЕДАКТИРОВАТЬ: пара существующих ответов очень похожи, в частности: https://stackoverflow.com/a/25526473/2407870.

2 голосов
/ 01 марта 2015
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).

1 голос
/ 11 сентября 2018

Наконец, ответ A PHP 7.1 + с константами, которые нельзя переопределить.

/**
 * An interface that groups HTTP Accept: header Media Types in one place.
 */
interface MediaTypes
{
    /**
    * Now, if you have to use these same constants with another class, you can
    * without creating funky inheritance / is-a relationships.
    * Also, this gets around the single inheritance limitation.
    */

    public const HTML = 'text/html';
    public const JSON = 'application/json';
    public const XML = 'application/xml';
    public const TEXT = 'text/plain';
}

/**
 * An generic request class.
 */
abstract class Request
{
    // Why not put the constants here?
    // 1) The logical reuse issue.
    // 2) Single Inheritance. 
    // 3) Overriding is possible.

    // Why put class constants here?
    // 1) The constant value will not be necessary in other class families.
}

/**
 * An incoming / server-side HTTP request class.
 */
class HttpRequest extends Request implements MediaTypes
{
    // This class can implement groups of constants as necessary.
}

Если вы используете пространства имен, завершение кода должно работать.

Однако, делая это, вы теряете возможность скрывать константы в пределах семейства классов (protected) или одного класса (private). По определению, все в Interface есть public.

Руководство по PHP: Интерфейсы

1 голос
/ 19 февраля 2011

Вчера я написал этот класс в своем блоге . Я думаю, что это может быть легко для использования в PHP-скриптах:

final class EnumException extends Exception{}

abstract class Enum
{
    /**
     * @var array ReflectionClass
     */
    protected static $reflectorInstances = array();
    /**
     * Массив конфигурированного объекта-константы enum
     * @var array
     */
    protected static $enumInstances = array();
    /**
     * Массив соответствий значение->ключ используется для проверки - 
     * если ли константа с таким значением
     * @var array
     */
    protected static $foundNameValueLink = array();

    protected $constName;
    protected $constValue;

    /**
     * Реализует паттерн "Одиночка"
     * Возвращает объект константы, но но как объект его использовать не стоит, 
     * т.к. для него реализован "волшебный метод" __toString()
     * Это должно использоваться только для типизачии его как параметра
     * @paradm Node
     */
    final public static function get($value)
    {
        // Это остается здесь для увеличения производительности (по замерам ~10%)
        $name = self::getName($value);
        if ($name === false)
            throw new EnumException("Неизвестая константа");
        $className = get_called_class();    
        if (!isset(self::$enumInstances[$className][$name]))
        {
            $value = constant($className.'::'.$name);
            self::$enumInstances[$className][$name] = new $className($name, $value);
        }

        return self::$enumInstances[$className][$name];
    }

    /**
     * Возвращает массив констант пар ключ-значение всего перечисления
     * @return array 
     */
    final public static function toArray()
    {
        $classConstantsArray = self::getReflectorInstance()->getConstants();
        foreach ($classConstantsArray as $k => $v)
            $classConstantsArray[$k] = (string)$v;
        return $classConstantsArray;
    }

    /**
     * Для последующего использования в toArray для получения массива констант ключ->значение 
     * @return ReflectionClass
     */
    final private static function getReflectorInstance()
    {
        $className = get_called_class();
        if (!isset(self::$reflectorInstances[$className]))
        {
            self::$reflectorInstances[$className] = new ReflectionClass($className);
        }
        return self::$reflectorInstances[$className];
    }

    /**
     * Получает имя константы по её значению
     * @param string $value
     */
    final public static function getName($value)
    {
        $className = (string)get_called_class();

        $value = (string)$value;
        if (!isset(self::$foundNameValueLink[$className][$value]))
        {
            $constantName = array_search($value, self::toArray(), true);
            self::$foundNameValueLink[$className][$value] = $constantName;
        }
        return self::$foundNameValueLink[$className][$value];
    }

    /**
     * Используется ли такое имя константы в перечислении
     * @param string $name
     */
    final public static function isExistName($name)
    {
        $constArray = self::toArray();
        return isset($constArray[$name]);
    }

    /**
     * Используется ли такое значение константы в перечислении
     * @param string $value
     */
    final public static function isExistValue($value)
    {
        return self::getName($value) === false ? false : true;
    }   


    final private function __clone(){}

    final private function __construct($name, $value)
    {
        $this->constName = $name;
        $this->constValue = $value;
    }

    final public function __toString()
    {
        return (string)$this->constValue;
    }
}

Использование:

class enumWorkType extends Enum
{
        const FULL = 0;
        const SHORT = 1;
}
1 голос
/ 25 апреля 2016

Я сделал библиотеку на основе ответа Брайана Клайна, она называется greg0ire / enum Наслаждайтесь!

1 голос
/ 14 апреля 2011

Это мой взгляд на "динамическое" перечисление ... так что я могу вызывать его с переменными, напр. из формы.

посмотрите обновленную версию ниже этого кодового блока ...

$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};
...