ENUM枚举

枚举的概念

枚举类,固定范围的健和值,比如星期,性别,状态,色值,错误码等.

比如有一个星期字典.

$weekDict = [
    /** 星期日 */
    0 => "Sunday",
    /** 星期一 */
    1 => "Monday",
    /** 星期二 */
    2 => "Tuesday",
    /** 星期三 */
    3 => "Wednesday",
    /** 星期四 */
    4 => "Thursday",
    /** 星期五 */
    5 => "Friday",
    /** 星期六 */
    6 => "Saturday",
];

枚举需要:

  1. 只读不可以修改值.
  2. 检查限制数值范围,比如星期的范围是0-6,不能随意输成9.
  3. 通过数值转换成名称,名称转数值
  4. 通过名称检测是否在范围内

一般数据库存储值,输出展示是名称.

PHP7之前实现

我们最简单做法,建一个字典,统一维护K-V.

class WeekEnum{
    const Sunday=0;
    const Monday=1;
    const Tuesday=2;
    const Wednesday=3;
    const Thursday=4;
    const Friday=5;
    const Saturday=6;
}

// 检测是否存在
if (defined('WeekEnum::Monday')){
    // 返回值
    echo WeekEnum::Monday;
}

但现在,值不受限范围.比如值为9时没法判断.
简单的改造一下.

class WeekEnum{
    const Sunday=0;
    const Monday=1;
    const Tuesday=2;
    const Wednesday=3;
    const Thursday=4;
    const Friday=5;
    const Saturday=6;

    const _dict=[
        self::Sunday=>'Sunday',
        self::Monday=>'Monday',
        self::Tuesday=>'Tuesday',
        self::Wednesday=>'Wednesday',
        self::Thursday=>'Thursday',
        self::Friday=>'Friday',
        self::Saturday=>'Saturday',
    ];
}

var_dump(defined('WeekEnum::Monday'));// 验证健
var_dump(WeekEnum::Monday);// 返回值

var_dump(isset(WeekEnum::_dict[9]));// 验证值,false
var_dump(WeekEnum::_dict[1]); // 返回健,Monday

现在基本能满足了大部需求. 唯一性也得到保证.
但是有时需要重复的值,还需要重复写dict.扩展不方便.

下面使用反射来完成.

class WeekEnum{
    const Sunday=1;
    const Monday=1;
    const Tuesday=2;
    const Wednesday=3;
    const Thursday=4;
    const Friday=5;
    const Saturday=6;

    protected static $dict=null;

    protected static function getCosts():array{
        if(!isset(static::$dict)) {
            $ref = new ReflectionClass(self::class);
            self::$dict = $ref->getConstants();
        }

        return (array)static::$dict;
    }
    /**
     * 验证值
     * @param $value
     * @return bool
     */
    public static function validValue($value): bool
    {
        return in_array($value,static::getCosts());
    }

    /**
     * 获取Key
     * @param $value
     * @return mixed
     */
    public static function getKey($value)
    {
        return array_search($value,static::getCosts());
    }

}


var_dump(WeekEnum::validValue(9)); // false
var_dump(WeekEnum::validValue(1));// true
var_dump(WeekEnum::getKey(1));// 重复返回第1个 Sunday
var_dump(WeekEnum::Friday);// 返回值 5

稍微改造一下.更简洁了.

abstract class Enum{
    protected static $dict=null;

    protected static function getCosts():array{
        if(!isset(static::$dict)) {
            $ref = new ReflectionClass(static::class);
            self::$dict = $ref->getConstants();
        }

        return (array)static::$dict;
    }
    /**
     * 验证值
     * @param $value
     * @return bool
     */
    public static function validValue($value): bool
    {
        return in_array($value,static::getCosts());
    }

    /**
     * 获取Key
     * @param $value
     * @return mixed
     */
    public static function getKey($value)
    {
        return array_search($value,static::getCosts());
    }
}

//--------------

// 定义一个枚举类
final class WeekEnum extends Enum{
    const Sunday=1;
    const Monday=1;
    const Tuesday=2;
    const Wednesday=3;
    const Thursday=4;
    const Friday=5;
    const Saturday=6;
}`
var_dump(WeekEnum::validValue(9)); // false
var_dump(WeekEnum::validValue(1));// true
var_dump(WeekEnum::getKey(1));// 重复返回第1个 Sunday
var_dump(WeekEnum::Friday);// 5

目前还不像其他语言一样enum.

完整的ENUM

要实现 可实例化.
(new Enum(1))->key,
(new Enum(1))->value,
(new Enum(1))->description

枚举web端常见的形式结构是.

<select>
  <option value="1">星期一</option>
  <option value="2">星期二</option>
  <option value="3">星期三</option>
</select>

key: 后端开发人员容易记住,类似host->ip一样
value: 实际存储值(方便计算,索引)
description: 别名,前端展示用,不同语言展示不同.

实例化某一条option,即一个枚举.
下面分别实现下面4种试实例化.

$item=new Enum(1);// 通过值初始化值
$item= Enum::formValue(1);// 通过值实例化
$item= Enum::formKey("Monday");// 通过KEY实例化
$item= Enum::Monday();// 同名函数实例化.

$item->key;
$item->value;
$item->description;

实现construct


abstract class Enum{
    // 省略...
    public $key;
    public $value;
    public $description;
    
    public function __construct($value){
        $this->value=$value;
        $this->key=self::getKey($value);
        $this->description=$this->key;
    }
    
    // 省略...
}

-w1274
-w501

实现 formValue()

public static function formValue($value){
        return new static($value);
}

测试:
-w1279

实现 fromKey()

public static function fromKey($key){
    $value=constant('static::'.$key);// 获取值
    return new static($value);
}

-w1252

实现 Key同名函数 XX()

利用 __callStatic 来实现

public static function __callStatic($key, $arguments)
{
    if(!defined('static::'.$key)){
        throw new Exception("未定义$key");
    }
    return static::fromKey($key);
}

测试:
-w1241

至止四种初始化枚举都已实现.只需要处理一些异常情况就完善.

实现只读属性

key,value 这些枚举属性避免使用者修改,我们需要私有化
但又需要让外面能访问

在PHP8.1之后,支持readonly 申明.

class Enum {
   public readonly mixed $value;// 只读
   public readonly mixed $key;// 只读
   public $description; // 可以
}

在php8.1后尝试修改只读属性会抛出异常.
-w1175

在 < php8.1 前 怎么实现?
通常都是用setter和getter

 class Enum {
    protected $value;
    protected $key;
    public $description; 
    
    public function setKey($key){
        $this->key=$key;
    }
    
    public function getKey(){
        return $this->key;
    }
}

避免命名冲突.get_set_来代替.

class Enum {
    public function get_key(){
        return $this->key;
    }

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

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

测试
-w1188

只能调用setKey和getKey获取数据.这很不php太java了.还容易命名冲突.
我们需要$obj->key这种形式.
php有__set,__get魔术方法

class Enum{
    protected $value;
    protected $key;
    protected $description;
    // 模拟只读
     public function __get($property){
        if(in_array($property,['value','key','description'])){
            return $this->{$property};
        }
        throw new \Error("Cannot access protected property ".__CLASS__.'::$'.$property);
    }
    
    public function __set($property,$value){
        if(in_array($property,['value','key','description'])){
            throw new \Error("Cannot modify readonly property".__CLASS__.'::$'.$property);
        }
    }

现在可以使用$obj->key;$obj->value;$obj->description,其他私有属性原样处理就可以.
g

-w640
现在正常可以限制修改,也能读取. 只是IDE不太友好,私有属性不能代码提示和补全.
可以使用注释@property来提示.支持读写@property只读@property-read,只写@property-write

/**
 * Enum
 * @property-read $value
 * @property-read $key
 * @property-read $description
 */
abstract class Enum{
    protected $value;
    protected $key;
    protected $description;
    
     public function __get($property){
        if(in_array($property,['value','key','description'])){
            return $this->{$property};
        }
        throw new \Error("Cannot access protected property ".__CLASS__.'::$'.$property);
    }
    
    public function __set($property,$value){
        if(in_array($property,['value','key','description'])){
            throw new \Error("Cannot modify readonly property".__CLASS__.'::$'.$property);
        }
    }
    
}

这样在运行阶段就能屏蔽错误,减少犯错可能.

实现注解

需要额外说明
实现注释,如下注解方式,我们可以利用反射获取注释,正则匹配出内容.
大于 php8.0 支持 注解. #[xxx]
final class WeekEnum extends Enum{

    #[Description("礼拜天")]
    /** @Description("星期日") */
    const Sunday=0;
    /** @Description("星期一") */
    const Monday=1;
    /** @Description("星期二") */
    const Tuesday=2;
    /** @Description("星期三") */
    const Wednesday=3;
    /** @Description("星期四") */
    const Thursday=4;
    /** @Description("星期五") */
    const Friday=5;
    /** @Description("星期六") */
    const Saturday=6;
}

通过反射获取注释.

 $ref=new \ReflectionClass(WeekEnum::class);
 // 获取某个常量
$const=$ref->getReflectionConstant("Sunday");
echo "名:",$const->getName(),PHP_EOL;
echo "值:",$const->getValue(),PHP_EOL;
// 获取文档注释
echo "注释:",toScalar($const->getDocComment()),PHP_EOL;
// 正则获取参数内容
preg_match("/@Description\((.*)\)/",$const->getDocComment(),$matches);
echo "注解值:",$matches[1],PHP_EOL;


// 转成标量
function toScalar(string $str){
    $scalar=json_decode('{"scalar":'.$str.'}');
    return $scalar->scalar??$str;
}

输出:

名:Sunday
值:0
注释:/**
     * @Description("星期天")
     */
注解值:星期天

正则解析获取特别复杂,一下小心就会错误,可以使用第三方类. 或使用php8.0的注解方式.
比如,需要考虑: 类型null,false|true,[1,2,3],{a:1,b:2},name="xxx"等等数值转化,参数切分.

在php8.0 注解如何使用?

$ref=new \ReflectionClass(WeekEnum::class);
$const=$ref->getReflectionConstant("Sunday");
// 注意在命名空间时要写全
$namespace = $ref->inNamespace()?$ref->getNamespaceName().'\\':'';
$atrrs=$const->getAttributes($namespace."Description");
$desc=$atrrs?$atrrs[0]:null;
// 参数不需要转成标量
echo $text=$desc?$desc->getArguments()[0]:null;// 礼拜天

在枚举在使用注解,修改getConst,添加附加描述信息.

class Enum {
    
    /** @var array 附加描述 */
    protected static ?array $meta;

    public function __construct($value){
        $this->value=$value;
        $this->key=static::getKey($value);
        // 获取注释逻辑
        $this->description=$this->description();
    }
    
    public function description(){
        return static::$meta[$this->key]['description']??$this->key;
    }

    /**
     * 获取所有常量
     * @return array
     */
    protected static function getCosts():array{
        if(isset(static::$dict)){
            return static::$dict;
        }
        $meta=[];
        $ref = new \ReflectionClass(static::class);

        foreach($ref->getReflectionConstants() as $const){
            $desc=null;
            
            // php8原生注解形式
            if(method_exists($const,'getAttributes')) {
                $namespace = $ref->inNamespace()?$ref->getNamespaceName().'\\':'';
                [$descAttr] = $const->getAttributes($namespace."Description")+[null];
                [$desc]= $descAttr?(array)$descAttr->getArguments()+[null]:[null];
            }
            
            // 普通注释方式
            if(!$desc && $doc=$const->getDocComment()){
                $desc=static::parseDoc($doc);
            }

            $meta[$const->getName()]=[
                'description'=>$desc
            ];
        }
        static::$meta=$meta;
        static::$dict=$ref->getConstants();

        return (array)static::$dict;
    }

    // 解析注释
    private static function parseDoc(string $doc){
        if(preg_match('/@Description\("(.*)"\)/',$doc,$params)
            && isset($params[1])){
            return $params[1];
        }

        return null;
    }
    

测试:
-w1102

扩展:
*. 文档注释是 /** xx */ ,如果其他注释怎么办?如 # xxx或者 /* xxx */

这时候就可以用AST解析方式,解析源码,过程很复杂,性能差.

*. 大量需要注解的地方,性能都很差,需要缓存解析结果.

*. php8注解

性能对比:
基于php8环境,测试10000*1000万次

-w1243

-w1261

原生注解和手写注解性能差不多.这个对比只是简单对比,因为注解的很多正则匹配没处理.

注解实现翻译

那原生注解除了方便,还有什么用呢?

定义注解类: #[Attribute()]


// 定义一个注解类
#[Attribute(Attribute::TARGET_ALL)]
class Lang {
    public $msg;
    // 2
    public function __construct($msg){
       $this->msg=$msg;// hello
    }
    // 4
    public function say(){
        echo $this->msg;
    }
}

class Enum {
    #[Lang("hello")]
    const Hello="hello";
}

$ref = new ReflectionClass(Enum::class);
// 1
$obj=$ref->getConstant("Hello")
        ->getAttribute("Lang")[0]
        ->newInstance();
// 3        
obj->say();

执行 newInstance 后会自动初始化 注解类 Lang 的类 .
相当于 new Lang(第一个参数,...参数) ,
#[Attribute(Attribute::TARGET_ALL)] 可以定义Target对象支持:类,方法,属性

完整示例:

#[Attribute(Attribute::TARGET_ALL)]
class Description {

   protected $text='';

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

   /**
    * 翻译
    * @return string
    */
   public function _(): string
   {
       $locale=getenv('locale')??'en_US';
       $langs=[
           "monday"=>['zh-CN'=>"星期一",'en_US'=>"Monday"],
           "tuesday"=>['zh-CN'=>"星期二",'en_US'=>"Tuesday"],
       ];
       return $langs[$this->text][$locale]??$this->text;
   }

   public function __toString(){
       return $this->_();
   }
}


final class Enum{
   #[Description("monday")]
   const Monday=1;
   #[Description("tuesday")]
   const Tuesday=2;

   protected static array $messages=[];
   protected static array $dict=[];

   public function __construct($value){
       $this->value=$value;
       $this->key=$this->getKey();
       $this->description=$this->getDescription();
   }

   public function getKey(){
       return array_search($this->value,self::$dict);
   }

   public function getDescription(){
       return self::$messages[$this->key];
   }
}

// 扫描
function scanEnum(){
   $ref = new ReflectionClass(Enum::class);
   $dict=$ref->getConstants();
   $ref->getProperty('dict')->setValue($dict);

   $messages=[];
   foreach($ref->getReflectionConstants() as $const){
       $obj=$const->getAttributes("Description")[0]->newInstance();
       $messages[$const->getName()]=(string)$obj;
   }
   $ref->getProperty('messages')->setValue($messages);
}

putenv('locale=zh-CN');
scanEnum();

$item=(new Enum(1));
echo $item->key;
echo $item->value;
echo $item->description;

PHP8.1 ENUM

从 PHP8.1f起已经支持,原生enum类型.
先看enum都有什么特性.
官方文档:
https://www.php.net/manual/zh/language.types.enumerations.php

总结

枚举的核心,唯一键值对,只读,
方便: 定义方便,取值方便,管理方便

实现原理:

  • 利用数组对像键唯一
  • 利用反射获取const键和值,注释

原作者:阿金
本文地址:https://hi-arkin.com/archives/php-enum.html

标签: enum 枚举类 php

(本篇完)

评论