PHP实现ENUM枚举类
ENUM枚举
枚举的概念
枚举类,固定范围的健和值,比如星期,性别,状态,色值,错误码等.
比如有一个星期字典.
$weekDict = [
/** 星期日 */
0 => "Sunday",
/** 星期一 */
1 => "Monday",
/** 星期二 */
2 => "Tuesday",
/** 星期三 */
3 => "Wednesday",
/** 星期四 */
4 => "Thursday",
/** 星期五 */
5 => "Friday",
/** 星期六 */
6 => "Saturday",
];
枚举需要:
- 只读不可以修改值.
- 检查限制数值范围,比如星期的范围是0-6,不能随意输成9.
- 通过数值转换成名称,名称转数值
- 通过名称检测是否在范围内
一般数据库存储值,输出展示是名称.
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;
}
// 省略...
}
实现 formValue()
public static function formValue($value){
return new static($value);
}
测试:
实现 fromKey()
public static function fromKey($key){
$value=constant('static::'.$key);// 获取值
return new static($value);
}
实现 Key同名函数 XX()
利用 __callStatic
来实现
public static function __callStatic($key, $arguments)
{
if(!defined('static::'.$key)){
throw new Exception("未定义$key");
}
return static::fromKey($key);
}
测试:
至止四种初始化枚举都已实现.只需要处理一些异常情况就完善.
实现只读属性
key,value 这些枚举属性避免使用者修改,我们需要私有化
但又需要让外面能访问
在PHP8.1之后,支持readonly 申明.
class Enum {
public readonly mixed $value;// 只读
public readonly mixed $key;// 只读
public $description; // 可以
}
在php8.1后尝试修改只读属性会抛出异常.
在 < 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;
}
}
测试
只能调用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
现在正常可以限制修改,也能读取. 只是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;
}
测试:
扩展:
*. 文档注释是 /** xx */
,如果其他注释怎么办?如 # xxx
或者 /* xxx */
这时候就可以用AST解析方式,解析源码,过程很复杂,性能差.
*. 大量需要注解的地方,性能都很差,需要缓存解析结果.
*. php8注解
性能对比:
基于php8环境,测试10000*1000万次
原生注解和手写注解性能差不多.这个对比只是简单对比,因为注解的很多正则匹配没处理.
注解实现翻译
那原生注解除了方便,还有什么用呢?
定义注解类: #[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