happy-types/enumerable-type

PHP中可枚举类型的强类型实现,帮助我们编写更安全、更易读的代码。

v1.0.2 2019-02-18 14:39 UTC

This package is auto-updated.

Last update: 2024-09-19 05:58:45 UTC


README

强类型实现PHP中的可枚举类型,有助于编写更安全、更易读的代码。

为什么需要创建它呢?

目前有许多枚举实现,但它们缺少一个核心功能——强类型支持
如果你看看你的代码,你会看到大多数业务逻辑操作将依赖于具有预定义选项的类型。
比如订单状态、航班状态、购买状态、票据类型、公司类型。很多情况下,业务逻辑会是这样:如果你的票据类型是“优先级”并且购买状态是“完成”,就执行一些操作。

使用这个库,你的PHP代码中的业务逻辑将看起来像这样

   if (($ticket->getType() === TicketType::Priority()) && 
       ($purchase->getStatus() === PurchaseStatus::Completed()) {
       // do your logic here...
   }
       

另一个例子是,你的逻辑几乎每次都依赖于特定的可枚举类型,你几乎100%想
确保有人会给你正确的可枚举类型的值。你有多少次调试是因为给定的选项与枚举的预期相同,但得到了一个完全不同的枚举,其中包含相同的选项(不是在概念上相同,而是在字面上或整数上相同)?

public function someMethod(CompanyType $companyType) {
 // ...
}

// usage:
someMethod(CompanyType::Private());
someMethod(CompanyType::fromId('private'));

不可能给出任何其他对象,只能给出CompanyType(例如,不是PersonType或RepositoryType,这些也有相同的'private'、'public'选项)。

这些枚举选项确实是特定类型的对象,每次你调用CompanyType::Private(),你都会得到表示可枚举类型CompanyType的“私有”选项的相同对象,因此你可以安全地通过引用来比较这些对象。例如,CompanyType::Private() === CompanyType::Private()总是为真。

这是按这种方式设计的,使枚举看起来像默认的PHP语言构造,易于创建和易于使用。

有什么好处?

  • 代码中没有硬编码的文本
  • 允许将方法的所有参数类型提示为只接受特定的可枚举类型
  • 所有获取器和返回类型都可以提示为返回特定的可枚举类型
  • 创建这些枚举对象的单一点。例如,只能通过CompanyType::fromId($companyTypeId)
  • 项目团队可以用可枚举类型来思考,而不是字符串/整数
  • 完全的IDE支持,如查找用法、重构等。
  • 易于列出可用的可枚举类型(例如,CompanyType::enum()返回一个包含可用CompanyType对象的数组)

是否已准备好用于生产?

是的,这段代码已经写了一年多,仍在生产环境中运行。

易于使用

只需创建一个你想要可枚举的类,并扩展EnumerableType

    class CompanyType extends EnumerableType {
       final public static function Unknown() { return static::get(null); }       
       final public static function Private() { return static::get('private'); }       
       final public static function Public() { return static::get('public'); }
    }
    
    class PaymentMethod extends EnumerableType {
       final public static function Unknown() { return static::get(null); }       
       final public static function Cash() { return static::get('cash'); }       
       final public static function CreditCard() { return static::get('credit_card'); }
    }
    
    class DeliveryStatus extends EnumerableType {
       final public static function Delivered() { return static::get(1, 'delivered'); }       
       final public static function NotDelivered() { return static::get(0, 'not_delivered'); }       
    }

就是这样!不再需要常量或原语,只需有效的对象,它支持强类型

API文档

要创建一个新的可枚举类型,你需要扩展EnumerableType类,并添加所需的所有选项方法。
方法必须是以下格式
final public static function YourOption() { return static::get('your_option_id', 'your_option_name'); }.

Your_option_name参数是可选的,如果未指定,则选项name将与id相等。

你创建的类将包含两个额外的静态方法

  • CompanyType::fromId($id) - 方法返回一个代表特定CompanyType选项的 选项对象
  • CompanyType::enum() - 方法将返回一个包含所有选项(作为 选项对象)的数组

您还应该使用您添加的 final public static 方法来检索 选项对象

任何 选项对象 都包含两个方法

  • id() - 返回一个选项 ID('your_option_id'),例如 CompanyType::Private()->id()
  • name() - 返回一个选项名称('your_option_name'),例如 CompanyType::Private()->name()

提示:选项 ID 和选项名称不仅限于 string 类型,只需将所需内容传递给 static::get(...) 即可。

工作原理说明

选项对象 将是给定可枚举类型的实例。例如,CompanyType::Private() 将返回代表 "Private" 选项的 CompanyType 对象。
注意:选项对象仅在每个选项上创建一次,并将作为选项的 1:1 表示。这意味着系统中不可能有两个对象代表相同的可枚举选项。这个库使用对象作为特定选项的身份。

通过遵守这个身份规则,我们可以将这些 选项对象 视为 CompanyType 类型(类似于:类的子类,但不需要有实际扩展的类)的 子类型
困惑?好吧,让我们再试一次,这里有一个使用仅类实现的 EnumerableType 的替代实现

	class CompanyEnum { }
    class CompanyEnum_Private extends CompanyEnum {}
    class CompanyEnum_Public extends CompanyEnum {}
    class CompanyEnum_Unknown extends CompanyEnum {}

使用这种结构可以实现相同的结果

    function fromId($id) {
    	switch ($id) {
        	case 'private': return new CompanyEnum_Private();
            case 'public': return new CompanyEnum_Public();
            case null: return new CompanyEnum_Unknown();
            default: throw new \RuntimeException("unhandled {$id}");
        }
    }
    
	function doSomething(CompanyEnum $companyEnum) {
         if ($companyEnum instanceof CompanyEnum_Private) {
           // ...
         }
    }

但在这种实现中,特定选项的身份基于 类名 而不是对象本身。
如果对象可以像这个示例中的子类一样精确地代表一个选项,那么我们可以应用相同的 "instanceof" 语义,而无需任何子类,只需使用严格相等(===)并避免任何子类化即可。

安装

`composer require happy-types/enumerable-type`

代码约定:大写字母 vs PascalCase

你可能刚刚看到了这个库示例中的一个“尴尬”(在 PHP 中不常见,但在 C# 中是默认的)方法命名,但这是有意为之。

团队中需要以某种方式标记这种“新”模式,以便任何 PHP 开发者都能清楚地识别代码中可枚举类型逻辑的位置。

有几个选择,其中一个是 UPPERCASE_NOTATION。但 UPPERCASE 变量已经有了意义——它是常量。
将方法全部大写写出来也很奇怪,例如 final public static function DO_NOT NEED_INSPECTION(){}
这还让我想起了旧的 C/C++ 时代(下划线变量和宏大写...)。

剩下什么呢?

常规方法命名 - camelCased。
常规方法命名是一个选项,但它没有表达出我们的方法有点特殊的事实——每个方法只返回一个唯一的对象——选项对象

因此,为了表达这些方法返回 选项对象 的事实,我们选择使用首字母大写的 camelCase,也称为 PascalCase。

因此,PascalCase 只是我们在团队内部使用的推荐编码风格。重要的是要选择一个并保持一致性。

附加信息

这里有一些我们使用 EnumerableType 开发的一些其他最佳实践

不要使用默认的 php serialize/unserialize for EnumerableType

使用默认 PHP 工具进行正确序列化目前是不可能的,因为我们无法强制对象唯一性。每次 PHP 反序列化一个对象时,它首先创建一个对象,然后设置值,我们必须确保对于给定的选项值,系统中只有一个唯一的对象。目前在 PHP 中这是不可能的。为 EnumerableType 类型的变量使用自定义序列化。

所有类的getter/setter都必须使用 EnumerableType 而不是字符串/整数

许多 PHP ORM 支持整数或字符串字段轻松持久化到数据库,但不支持自定义类对象。因此,需要在类的 getter/setter 中包装 EnumerableType 对象。

class Company {
   /**
   * @var int
   */
   private $companyTypeId;
   
   public function setCompanyType(CompanyType $value)
   {
      $this->companyTypeId = $value->id();
   }
   
   /**
   * @return CompanyType
   */
   public function getCompanyType()
   {
      return CompanyType::fromId($this->companyTypeId);
   }
}

所有使用 EnumerableType 的逻辑必须处理所有可能的情况(所有可用选项)

最好的做法是使用 switch 语句加上 default 情况。

   switch ($companyType) {
      case CompanyType::Private():
         // ... do logic..
         break;
      case CompanyType::Public():
         // ... do logic..
         break;
      default:
         throw new \RuntimeException("unhandled case:" . $companyType->name());
   }

这种代码风格将有助于你在未来遇到新的类型选项时。

不要直接在代码中使用 id 或 name。相反,直接使用严格相等性比较你的对象。

库是按照每次选项都会得到一个唯一的 对象 的方式编写的。对于相同的选项,对象将是相同的,因此您可以直接比较它们。

// BAD CODE:
   if ($companyType->id() === CompanyType::Private()->id()) { /*...*/ }
// AWFUL CODE:
   if ($companyType->id() === 'private') { /*...*/ }
// GOOD CODE:
   if ($companyType === CompanyType::Private()) { /*...*/ }

您可以轻松地获取您的 EnumerableType 中所有可用的选项。不要手动枚举。

有时需要列出您的枚举的所有可用选项。

// BAD CODE:
   $companyTypes = [CompanyType::Private(), CompanyType::Public()];
// GOOD CODE:
   $companyTypes = CompanyType::enum();
   foreach ($companyTypes as $companyType) {
        echo $companyType->name();
   }

不需要创建 EnumerableType 的手动工厂,只需使用 "fromId"。

// BAD CODE:
   switch ($companyTypeId) {
       case 'private': return CompanyType::Private();
       case 'public': return CompanyType::Public();
       default: return null;
   }
// GOOD CODE:
    $companyType = CompanyType::fromId($companyTypeId);

实际上,在可枚举类型中创建 "Unknown" 选项比使用 "null" 值更好。

对于使用枚举类型的类中的 getter/setter,没有必要返回 null。更好的做法是在枚举中创建 "Unknown" 选项。这样,您就可以在将来编写更漂亮的代码。

// BAD CODE:
    class PaymentMethod extends EnumerableType {
       final public static function Cash() { return static::get('cash'); }       
       final public static function CreditCard() { return static::get('credit_card'); }
    }
    function getPaymentMethod() {
       return $id ? PaymentMethod::fromId($id) : null;
    }
// GOOD CODE:
    class PaymentMethod extends EnumerableType {
       final public static function Unknown() { return static::get(null); }       
       final public static function Cash() { return static::get('cash'); }       
       final public static function CreditCard() { return static::get('credit_card'); }
    }
    function getPaymentMethod() {
       return PaymentMethod::fromId($id);
    }

结束

通过这次贡献,我期待着有一天 PHP 中可枚举类型的用法会标准化。

快乐类型!快乐的编码!

Antanas A. (antanas.arvasevicius@gmail.com)