类型安全时代:PHP 8+ 的联合类型、交集类型与泛型(模板)最佳实践
PHP 正在经历一场静悄悄但深刻的转型:从"弱类型脚本语言"走向"现代类型安全语言"。
自 PHP 8.0 起,类型系统被系统性地补齐:联合类型、静态分析、只读属性、枚举、DNF 类型......再到 PHP 8.4 的属性钩子(Property Hooks) 与不对称可见性 ,类型不再只是"文档",而是可验证、可推导、可约束的契约。
本文将系统梳理 PHP 8+ 的类型能力,并结合真实工程场景 与静态分析工具(PHPStan / Psalm),给出一套可落地的最佳实践。
一、为什么类型安全对 PHP 如此重要?
在 Laravel、Symfony、Hyperf 等现代框架中,类型已经渗透进:
- 控制器方法签名
- DTO / VO / 领域模型
- 服务层接口
- 集合与管道
- 数据库映射(ORM)
类型安全带来的收益:
| 收益 | 说明 |
|---|---|
| 更早发现 Bug | 在 IDE / CI 阶段而非线上 |
| 更强的重构信心 | 重命名、抽取、内联 |
| 自文档化 | 方法签名即契约 |
| 更好的 IDE 体验 | 自动补全、跳转、重构 |
| 降低认知负担 | 减少 null / mixed 猜测 |
✅ 结论:类型不是"限制",而是"放大生产力"的工具。
二、联合类型(Union Types):从 |null 到复杂结构
1. 基础用法
function findUser(int $id): User|null
{
return UserRepository::find($id);
}
相比旧写法:
/**
* @return User|null
*/
✅ 优势:
- 语言级支持
- 运行时 + 静态分析双重校验
- 无需依赖 PHPDoc
2. 常见联合类型模式
2.1 成功 / 失败语义
function parse(string $input): Result|Error
比 mixed 或异常更明确。
2.2 多实现接口
function render(ResponseInterface|Htmlable $response): string
⚠️ 注意:联合类型不是子类型多态的替代品,接口仍是首选。
3. 反模式:滥用联合类型
❌ 过度宽松
function process(mixed|array|object $data): void
❌ "上帝类型"
string|int|bool|array|object
✅ 改进方式:
- 使用 DTO
- 使用枚举
- 使用接口抽象
三、交集类型(Intersection Types):能力的"与"
1. 基本语义
同时满足多个接口
function save(
LoggerAwareInterface&CacheableInterface $service
): void
该参数必须同时实现两个接口。
2. 典型使用场景
2.1 可缓存 + 可日志的中间件
interface CacheableInterface
{
public function getCacheKey(): string;
}
interface LoggerAwareInterface
{
public function setLogger(LoggerInterface $logger): void;
}
class RequestMiddleware implements
CacheableInterface,
LoggerAwareInterface
{
}
2.2 与泛型结合(伪泛型)
/** @template T of ArrayAccess&Countable */
class Collection
{
/** @param T $storage */
public function __construct(private $storage) {}
}
✅ 交集类型非常适合描述能力组合,而不是"数据形状"。
3. 与联合类型的对比
| 类型 | 语义 |
|---|---|
| `A | B` |
A&B |
是 A 且 B |
⚠️ **DNF 类型(PHP 8.2+)** 允许组合:
(User&Identifiable)|AdminUser
四、DNF 类型:复杂业务语义的终极表达
1. 什么是 DNF?
Disjunctive Normal Form(析取范式)
(User&Verified)|GuestUser
表示:
- 已验证的普通用户
- 或访客用户
2. 实战示例:权限系统
function canAccessAdmin(
(User&Active&Verified)|SuperAdmin $user
): bool
{
return true;
}
✅ 比注释、策略类更直观
❌ 不适合过度复杂的业务规则(此时应建模为对象)
五、泛型(Generics):PHP 的"模板"现实
PHP 没有运行时泛型 ,但静态分析泛型已经成熟。
1. 使用 PHPDoc(事实标准)
/**
* @template T
* @param class-string<T> $class
* @return T
*/
function resolve(string $class)
{
return app($class);
}
2. 集合类(最典型场景)
/**
* @template T
* @extends IteratorAggregate<T>
*/
class Collection implements IteratorAggregate
{
/** @param T $item */
public function add($item): void {}
/** @return T */
public function first(): mixed {}
}
/** @var Collection<User> $users */
$users = new Collection();
3. 泛型约束(extends)
/**
* @template T of EntityInterface
*/
class Repository
{
/** @param T $entity */
public function save($entity): void {}
}
4. 为什么 PHP 没有"真正泛型"?
| 原因 | 说明 |
|---|---|
| 运行时成本 | 泛型会复制类结构 |
| 历史包袱 | PHP 是动态语言 |
| 工程现实 | PHPStan / Psalm 已足够 |
✅ 结论 :泛型是给人和工具看的,不是给 Zend Engine 看的。
六、只读、不可空与构造器属性提升
1. 只读属性(PHP 8.1+)
class User
{
public function __construct(
public readonly int $id,
public readonly string $email,
) {}
}
✅ 不可变对象
✅ 天然线程安全(协程)
✅ 强领域模型语义
2. 不可空类型(PHP 7.1+)
function sendEmail(string $to): void
⚠️ 永远不要返回
null表示失败,改用异常或 Result 类型。
七、静态分析:让类型"真正安全"
1. 工具选择
| 工具 | 特点 |
|---|---|
| PHPStan | 易上手、Laravel 友好 |
| Psalm | 更严格、更学术 |
| Phan | 老牌、偏底层 |
2. 推荐配置
# phpstan.neon
parameters:
level: 8
checkMissingIterableValueType: true
✅ 目标 :零 mixed,零 any
3. 渐进式类型策略
- 新代码:全类型 + 泛型
- 旧代码:PHPDoc → 原生类型
- 核心模型:严格泛型 + 不可变
八、最佳实践清单(直接可用)
✅ DO
- 用联合类型替代
|null注释 - 用交集类型描述能力组合
- 用 DNF 表达复杂业务规则
- 用泛型约束集合与仓库
- 用
readonly构建不可变对象 - 用 PHPStan 8 级作为底线
❌ DON'T
- 滥用
mixed - 用联合类型替代接口
- 用泛型"模拟动态语言"
- 在业务层暴露底层数据结构
九、结语:PHP 的类型未来
PHP 正在变成一种**"带动态能力的强类型语言"**:
- ✅ 运行时类型校验
- ✅ 编译期静态分析
- ✅ 可推导、可约束、可组合
类型不是 PHP 的枷锁,而是它走向工程化、企业级、长期维护的门票。
未来的 PHP 代码,将不再是:
function handle($data) {}
而是:
function handle(
Collection<User>&NotEmpty $users
): Result<Report> {}