news 2026/9/11 15:02:54

PHP 值对象实战指南:避免原始类型偏执

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP 值对象实战指南:避免原始类型偏执

PHP 值对象实战指南:避免原始类型偏执

上一篇文章里,我们聊了原始类型偏执(Primitive Obsession)在 PHP 里为什么这么常见:邮箱、金额、日期、ID……统统用 string/int/float/array 传来传去。领域含义被抹平,校验逻辑散落在各处,代码越写越难改。

这一篇我们继续往下走:值对象(Value Object)不仅能让代码更清晰,还能让协作、测试和后续演进都更省心。不管你用的是 Laravel、Symfony 还是别的框架,只要项目里有明确的领域概念,值对象都能派上用场。

原文链接 PHP 值对象实战指南:避免原始类型偏执

1. 重复出现的模式:不只是代码味道

在 PHP 项目里,某些规则会反复出现。日期就是典型例子:

publicfunctionregisterEvent(string$eventDate,string$timeZone):void{if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$eventDate)){thrownewInvalidArgumentException('Invalid date format.');}if(!in_array($timeZone,DateTimeZone::listIdentifiers())){thrownewInvalidArgumentException('Invalid time zone.');}// More logic here...}

这里的日期和时区都用 string 表示。问题在于:一旦日期格式要改、或者时区规则有变化,你就会开始在各个角落复制粘贴同样的验证逻辑——漏一处就出事故。

2. 值对象登场:Date 和 TimeZone

与其在每个入口都手写校验,不如把“日期”“时区”做成值对象,让它们自己保证合法性。

2.1 Date 值对象

finalclassDate{privatestring$value;privatefunction__construct(string$date){if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)){thrownewInvalidArgumentException('Invalid date format.');}$this->value=$date;}publicstaticfunctionfromString(string$date):self{returnnewself($date);}publicfunctionvalue():string{return$this->value;}publicfunction__toString():string{return$this->value;}}

2.2 TimeZone 值对象

finalclassTimeZone{privatestring$value;privatefunction__construct(string$timeZone){if(!in_array($timeZone,DateTimeZone::listIdentifiers())){thrownewInvalidArgumentException('Invalid time zone.');}$this->value=$timeZone;}publicstaticfunctionfromString(string$timeZone):self{returnnewself($timeZone);}publicfunctionvalue():string{return$this->value;}publicfunction__toString():string{return$this->value;}}

2.3 在业务里怎么用

publicfunctionregisterEvent(Date$eventDate,TimeZone$timeZone):void{// No need for repetitive validation// Logic continues...}

参数一眼就能看懂,而且验证逻辑只存在一份:在值对象里。

3. 给值对象加上行为

值对象不只是“更强的类型”。它还能承载和这个概念紧密相关的行为。

比如你需要判断活动日期是否在未来,可以把逻辑放进 Date:

finalclassDate{// ..publicfunctionisInTheFuture():bool{$now=newDateTime();$eventDate=newDateTime($this->value);return$eventDate>$now;}}

这样就不用在每个用到日期的地方都重复写一遍比较逻辑,也更符合领域表达:判断未来与否,本来就是“日期”这个概念的一部分。

4. 金额:用值对象守住精度

处理金额是原始类型偏执最容易踩坑的地方之一。用 float 表示钱,舍入误差迟早会找上门;再加上币种、汇率,复杂度会迅速拉高。

把金额做成值对象,通常的做法是:

  • 用最小单位(比如分)把金额存成 int
  • 币种作为字段和金额绑定在一起

下面这个 Money 示例进一步加上了换汇的行为:

finalclassMoney{privateint$amount;// Stored in minor units (e.g., cents)privatestring$currency;// Constructor and other methods...publicfunctionconvertToCurrency(string$targetCurrency,float$exchangeRate):self{$convertedAmount=(int)round($this->amount*$exchangeRate);returnnewself($convertedAmount,$targetCurrency);}}

把规则关在 Money 里,你的业务代码就不用到处关心“这里是分还是元”“币种对不对”“舍入怎么做”。

5. 在框架里落地

一旦你开始用值对象,Laravel / Symfony 反而会更好用:你能把“原始数据 ↔ 值对象”的转换放到框架扩展点里,业务层拿到的就都是领域类型。

5.1 Laravel 示例

Laravel 里可以用自定义 cast,把数据库字段自动转成值对象:

useIlluminate\Contracts\Database\Eloquent\CastsAttributes;classMoneyCastimplementsCastsAttributes{publicfunctionget($model,string$key,$value,array$attributes){returnMoney::fromInt($value,'USD');}publicfunctionset($model,string$key,$value,array$attributes){return$valueinstanceofMoney?$value->amount():$value;}}

把这个 cast 挂到 Eloquent 模型上后,取出来的就是 Money,而不是裸值,代码会干净很多。

5.2 Symfony 示例

Symfony 里可以用 Doctrine 的 embeddables 或自定义 DBAL type,把 Money / EmailAddress 这类复杂类型映射到数据库:

classMoneyTypeextends\Doctrine\DBAL\Types\Type{constMONEY='money';// Custom type namepublicfunctionconvertToPHPValue($value,\Doctrine\DBAL\Platforms\AbstractPlatform$platform){returnMoney::fromInt($value,'USD');}publicfunctionconvertToDatabaseValue($value,\Doctrine\DBAL\Platforms\AbstractPlatform$platform){return$valueinstanceofMoney?$value->amount():$value;}}

这样做的好处是:从数据库到领域层,你始终在用“领域类型”,而不是一堆无意义的 string/int/float。

6. 怎么迁移现有代码库

引入值对象不需要推倒重来。最稳妥的方式是渐进式迁移:

  • 先改边界层:在 HTTP controller、表单请求、CLI 命令里,把输入的原始值解析成值对象
  • 再改服务层:逐步把 service 方法签名从原始类型换成值对象
  • 配合静态分析:用 PHPStan 或 Psalm 强化类型约束,尽早发现不匹配

7. 结语:让领域自己说话

值对象的意义,不在于“OO 更纯粹”,而在于让领域概念变得清楚、可约束、可复用。

下次你准备在方法里传一堆 string/int 的时候,不妨停一下问自己:

“这真的是一个简单值吗?还是一个应该被命名、被约束的领域概念?”

做出这个小改变,短期能减少重复校验和隐性 bug;长期则会让整个代码库更稳、更好改——也更照顾未来维护它的你。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 20:10:32

BIThesis LaTeX模板:2025新规适配的零基础配置方法

BIThesis LaTeX模板:2025新规适配的零基础配置方法 【免费下载链接】BIThesis 📖 北京理工大学非官方 LaTeX 模板集合,包含本科、研究生毕业设计模板及更多。🎉 (更多文档请访问 wiki 和 release 中的手册)…

作者头像 李华
网站建设 2026/9/11 14:23:05

24、深入了解Linux设备驱动与网络设备

深入了解Linux设备驱动与网络设备 1. 网络设备概述 网络设备与普通设备不同,它不通过设备节点访问,也没有主设备号和次设备号。内核会根据字符串和实例编号为网络设备分配名称。例如,网络驱动注册接口的示例代码如下: my_netdev = alloc_netdev(0, "net%d", …

作者头像 李华
网站建设 2026/9/12 7:36:51

如何评估EmotiVoice生成语音的质量?主观+客观双标准

如何评估 EmotiVoice 生成语音的质量?主观 客观双标准 在智能语音内容需求爆发的今天,用户早已不再满足于“能说话”的机器朗读。无论是有声书、虚拟主播,还是游戏 NPC 和陪伴型 AI 助手,大家期待的是会表达、有性格、像真人的声…

作者头像 李华
网站建设 2026/9/12 6:43:39

用EmotiVoice为游戏角色注入灵魂级语音表现

用EmotiVoice为游戏角色注入灵魂级语音表现 在一款沉浸式角色扮演游戏中,当玩家踏入古老遗迹的深处,守卫者低沉而颤抖的声音从阴影中传来:“你竟敢闯入这片禁地!”——如果这句台词只是机械朗读,再宏大的场景也会瞬间失…

作者头像 李华
网站建设 2026/9/10 8:43:18

3、网络渗透测试侦察阶段实用技巧与工具使用指南

网络渗透测试侦察阶段实用技巧与工具使用指南 在网络渗透测试中,侦察阶段是至关重要的一环。它能帮助我们尽可能多地收集目标的信息,从而为后续发现和利用漏洞提供更多机会。以下将详细介绍侦察阶段常用的工具和方法。 1. 使用Nmap扫描和识别服务 Nmap是全球最常用的端口扫…

作者头像 李华
网站建设 2026/9/11 6:26:57

4、网络渗透测试中的信息收集与工具使用

网络渗透测试中的信息收集与工具使用 在网络渗透测试中,信息收集是至关重要的一步,它能帮助测试人员了解目标系统的各种信息,为后续的攻击提供基础。以下将介绍一些常用的工具和方法,包括密码分析、字典生成、文件和文件夹查找以及网页爬取等。 1. 常见HTTP状态码 在网络…

作者头像 李华