dbalabka/php-enumeration

PHP 中枚举类的实现。枚举的更好替代品

v0.4.0 2020-05-03 19:52 UTC

README

Build Status Coverage Status

在 PHP 中实现 枚举类。枚举的更好替代品。

与现有解决方案相比,此实现避免使用 魔术方法反射,以提供更好的性能和代码自动完成。枚举类持有单个枚举元素的单例对象引用,以提供值之间的严格(===)比较的可能性。它还使用静态属性,可以利用 类型属性 的力量。枚举类与其他语言实现(如 Java 枚举Python 枚举)非常相似。

声明

声明命名枚举类的基本方式

<?php
use Dbalabka\Enumeration\Enumeration;

final class Action extends Enumeration
{
    public static $view;
    public static $edit;
}
Action::initialize();

注意!应该在枚举类声明后立即调用 Enumeration::initialize() 方法。为了避免手动初始化,可以使用此库中提供的 StaticConstructorLoader

支持类型属性的声明

<?php
final class Day extends Enumeration
{
    public static Day $sunday;
    public static Day $monday;
    public static Day $tuesday;
    public static Day $wednesday;
    public static Day $thursday;
    public static Day $friday;
    public static Day $saturday; 
}
Day::initialize();

默认情况下,枚举类不需要提供值。您可以使用构造函数设置任何类型的值。

  1. 标志枚举实现示例
    <?php
    final class Flag extends Enumeration
    {
        public static Flag $ok;
        public static Flag $notOk;
        public static Flag $unavailable;
    
        private int $flagValue;
    
        protected function __construct()
        {
            $this->flagValue = 1 << $this->ordinal();
        }
    
        public function getFlagValue() : int
        {
            return $this->flagValue;
        }
    }
  2. 应该覆盖 initializeValues() 方法以设置每个枚举元素的值。
    <?php
    
    final class Planet extends Enumeration
    {
        public static Planet $mercury;
        public static Planet $venus;
        public static Planet $earth;
        public static Planet $mars;
        public static Planet $jupiter;
        public static Planet $saturn;
        public static Planet $uranus;
        public static Planet $neptune;
    
        private float $mass;   // in kilograms
        private float $radius; // in meters
    
        // universal gravitational constant  (m3 kg-1 s-2)
        private const G = 6.67300E-11;
    
        protected function __construct(float $mass, float $radius)
        {
            $this->mass = $mass;
            $this->radius = $radius;
        }
        
        protected static function initializeValues() : void
        {
            self::$mercury = new self(3.303e+23, 2.4397e6);
            self::$venus   = new self(4.869e+24, 6.0518e6);
            self::$earth   = new self(5.976e+24, 6.37814e6);
            self::$mars    = new self(6.421e+23, 3.3972e6);
            self::$jupiter = new self(1.9e+27,   7.1492e7);
            self::$saturn  = new self(5.688e+26, 6.0268e7);
            self::$uranus  = new self(8.686e+25, 2.5559e7);
            self::$neptune = new self(1.024e+26, 2.4746e7);
        }
    
        public function surfaceGravity() : float 
        {
            return self::G * $this->mass / ($this->radius * $this->radius);
        }
    
        public function surfaceWeight(float $otherMass) {
            return $otherMass * $this->surfaceGravity();
        }
    }

开发者应遵循的声明规则

  1. 生成的枚举类应标记为 final。应使用抽象类在多个枚举类之间共享功能。

    ...允许枚举成员定义的枚举的子类化将违反类型和实例的一些重要不变性。另一方面,允许在枚举组之间共享一些共同行为是有意义的...(来自 Python 枚举文档

  2. 构造函数应始终声明为非公共(privateprotected),以避免意外的类实例化。
  3. 实现基于假设所有类静态属性都是枚举的元素。
  4. 应该在每次枚举类声明后调用 Dbalabka\Enumeration\Enumeration::initialize() 方法。请使用此库中提供的 StaticConstructorLoader 以避免样板代码。

用法

基本用法

<?php
use Dbalabka\Enumeration\Examples\Enum\Action;

$viewAction = Action::$view;

// it is possible to compare Enum elements
assert($viewAction === Action::$view);

// you can get Enum element by name 
$editAction = Action::valueOf('edit');
assert($editAction === Action::$edit);

// iterate over all Enum elements
foreach (Action::values() as $name => $action) {
    assert($action instanceof Action);
    assert($name === (string) $action);
    assert($name === $action->name());
}

匹配表达式

PHP 8 将支持 匹配表达式,这简化了枚举的使用

<?php
use Dbalabka\Enumeration\Examples\Enum\Action;

$action = Action::$view;

echo match ($action) {
   Action::$view => 'View action',
   Action::$edit => 'Edit action',
}

更多用法示例

已知问题

只读属性

在当前实现中,静态属性值有时会被替换。 只读属性 旨在解决此问题。在理想世界中,枚举值应声明为常量。不幸的是,PHP 现在还不允许这样做。

<?php
// It is possible but don't do it
Action::$view = Action::$edit;
// Following isn't possible in PHP 7.4 with declared properties types
Action::$view = null;

还可以参考最新的 一次写入属性 RFC,该 RFC 旨在解决这个问题。

类静态初始化

此实现依赖于类静态初始化,该初始化在 静态类构造函数 中提出。该 RFC 仍处于草案状态,但它描述了可能的解决方案。最简单的方法是在类声明后立即调用初始化方法,但这要求开发者牢记这一点。多亏了 类型属性,我们可以控制未初始化的属性 - 如果访问未初始化的属性,PHP 将抛出错误。这可以通过此库中提供的自定义自动加载器 Dbalabka\StaticConstructorLoader\StaticConstructorLoader 自动化

<?php 
use Dbalabka\StaticConstructorLoader\StaticConstructorLoader;

$composer = require_once(__DIR__ . '/vendor/autoload.php');
$loader = new StaticConstructorLoader($composer);

此外,表达式初始化属性将非常有帮助(参见 C# 示例

class Enum {
    // this is not allowed
    public static $FOO = new Enum();
    public static $BAR = new Enum();
}

例如,请参阅 examples/class_static_construct.php

序列化

无法序列化单例。因此,我们必须限制直接枚举对象序列化。

<?php
// The following line will throw an exception
serialize(Action::$view);

新的自定义对象序列化机制 对单例序列化没有帮助,但它为在包含枚举实例引用的类中控制此问题提供了可能性。此外,可以通过类似的方式使用 Serializable 接口 来解决这个问题。例如,Java 处理 枚举序列化与其他类不同,但可以通过 readResolve() 直接序列化。在 PHP 中,我们无法直接序列化枚举,但可以在包含引用的类中处理枚举序列化。我们可以序列化枚举常量的名称,并在反序列化期间使用 valueOf() 方法获取枚举常量值。因此,这个问题在一定程度上解决了更差的开发者体验的代价。希望它将在未来的 RFC 中得到解决。

class SomeClass
{
    public Action $action;

    public function __serialize()
    {
        return ['action' => $this->action->name()];
    }

    public function __unserialize($payload)
    {
        $this->action = Action::valueOf($payload['action']);
    }
}

请参阅 examples/serialization_php74.php 中的完整示例。

可调用静态属性语法

不幸的是,使用存储在静态属性中的可调用对象并不容易。自 PHP 7.0 以来,发生了 语法更改,这使调用可调用的方式变得复杂。

// Instead of using syntax
Option::$some('1'); // this line will rise an error "Function name must be a string"
// you should use 
(Option::$some)('1');

这是静态类属性的主要缺点。

可以使用 __callStatic 的魔法调用进行解决,但这会导致缺少自动提示、性能影响和缺少静态分析。

Option::some('1');

将会有助于拥有 PHP 内置对延迟(在运行时)常量初始化或/和类常量初始化的支持,使用简单的表达式

class Enum {
   // this is not allowed
   public const FOO = new Enum();
   public const BAR = new Enum();
}

然而,调用 Enum::FOO() 将会尝试查找一个方法,而不是将常量的值视为可调用的。我们假设,这种PHP行为可以改进。

现有解决方案

PHP本地

(还有许多其他的PHP实现)

参考资料