happy-types / enumerable-type
PHP中可枚举类型的强类型实现,帮助我们编写更安全、更易读的代码。
Requires
- php: ^5.4 || ^7.0
Requires (Dev)
- phpunit/phpunit: ^4.8 || ^5.6
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)