PHP 8 带来的联合类型并不是单纯的语言糖,它改变了函数契约的书写方式。过去在方法签名中表达“这个参数可以是 A 也可以是 B”,只能依赖接口、抽象类、可变参数或者直接放弃类型约束,再通过 phpdoc 注释描述。这些做法要么要求不相关的类强行实现同一个接口,要么让 IDE 和静态分析工具失去精确的提示能力。联合类型把这类需求变成了原生语法,对传统设计模式的实现方式产生了直接影响。本文围绕策略模式、工厂模式、建造者模式和访问者模式,说明联合类型在哪些位置能够简化代码,哪些位置不应该替代接口抽象。

设计模式的核心往往是在类型之间建立可替换关系。联合类型和接口达成多态的路径不同:接口要求类型共享行为,联合类型只要求类型属于一个集合。这个差异决定了它在模式实现中的角色。
一、联合类型作为轻量级多态的边界
在 PHP 8 之前,如果某个函数需要接收多个不相关类型的参数,常见做法有两种:第一种是定义一个统一接口,让所有可能的类型都实现它;第二种是放弃类型约束,把参数声明为 mixed,再在函数内部手动判断。第一种方式的问题是容易产生大量包装类,第二种方式则完全失去了静态类型检查的保障。联合类型给出了第三种选择。例如一个日志写入函数需要接收字符串、可转换为字符串的对象或者异常对象,可以直接声明为 string|Stringable|Throwable。它不要求 Throwable 和 Stringable 之间产生任何继承关系,只是如实描述函数能够处理的值集合。
传统写法通常需要先定义一个 LogMessage 接口,再让字符串日志和异常日志分别实现它。这样虽然类型安全,但每个具体类型都要包装一次。换成联合类型后,函数签名可以更加直接:
<?php
interface LogMessage
{
public function toLog(): string;
}
class StringLog implements LogMessage
{
public function __construct(private string $text) {}
public function toLog(): string
{
return $this->text;
}
}
function writeLog(LogMessage $msg): void
{
echo $msg->toLog();
}
如果只是为了把异常和普通字符串交给同一个写入函数,上面的接口抽象就显得过重。使用联合类型可以省掉包装类,直接在函数内部用 match 表达式做类型收窄:
function writeLog(string|Stringable|Throwable $msg): void
{
$text = match (true) {
is_string($msg) => $msg,
$msg instanceof Throwable => $msg->getMessage(),
default => (string) $msg,
};
}
不过联合类型并不能替代接口。接口表达的是“这些类都能执行某个方法”,而联合类型表达的是“这些类都可能出现在这个位置”。如果后续需要通过统一方法调用对象,例如调用 $logObject->toLog(),那么仍然需要让它们实现同一个接口。换句话说,联合类型适合“窄入口多分支”的场景,不适合“统一行为调用”的场景。
二、策略模式与工厂模式的联合类型重构
策略模式的经典实现是定义一个策略接口,所有策略类实现该接口中的方法。当策略之间确实具有一致的行为契约时,这种抽象非常合理。但如果策略的输入参数或返回结果类型各不相同,统一接口就可能被迫放宽类型声明,或者引入额外的适配器。支付回调处理是一个典型例子:不同支付渠道返回的结果对象结构不同,AlipayResult、WechatResult、UnionPayResult 之间没有共同方法,只是为了类型提示而强行抽象出一个 PaymentResultInterface,意义并不大。此时可以直接把参数类型写成联合类型:
interface PaymentGateway
{
public function process(AlipayResult|WechatResult|UnionPayResult $result): string;
}
工厂模式中的简化更加明显。传统工厂方法通常返回一个接口或抽象类,但如果调用方只关心工厂能创建哪些具体类,并且这些类之间没有统一行为需要调度,那么返回联合类型能够提供更精确的信息。例如通知工厂根据渠道创建不同的通知器,可以直接声明返回类型为 EmailNotifier|SmsNotifier|PushNotifier:
function createNotifier(string $channel): EmailNotifier|SmsNotifier|PushNotifier
{
return match ($channel) {
'email' => new EmailNotifier(),
'sms' => new SmsNotifier(),
'push' => new PushNotifier(),
default => throw new InvalidArgumentException('Unknown channel'),
};
}
这种写法让调用方一眼就能看到工厂可能返回哪些具体类型,而不需要查看接口实现或文档注释。同时,配合 match 表达式可以做到穷尽性检查。但联合类型也意味着类型集合是封闭的:每新增一个通知渠道,都要修改方法签名。因此它更适合通知渠道、支付方式、日志级别这类相对稳定的封闭集合。对于插件体系、可扩展框架这类开放集合,还是应该使用接口作为扩展点。
三、建造者模式与访问者模式的简化与边界
建造者模式主要依赖流式接口和返回 static 来支持链式调用。PHP 8 的 static 返回类型让继承场景下的链式调用更加准确。在参数层面,联合类型可以处理建造者方法中常见的“一个配置项允许多种表示”的情况。例如设置超时时间时,既希望支持整数秒,也希望支持 DateInterval 对象。传统做法要么为 DateInterval 单独写一个方法,要么使用 mixed 再靠文档说明。现在可以直接声明为 int|DateInterval:
class HttpClientBuilder
{
private int $timeout = 5;
public function timeout(int|DateInterval $timeout): static
{
$this->timeout = $timeout instanceof DateInterval
? $timeout->s
: $timeout;
return $this;
}
}
访问者模式是双重分发的典型实现。传统上由于 PHP 不支持方法重载,访问者接口只能为每种元素类型定义不同的方法,比如 visitA、visitB、visitC,或者在单个 visit 方法里做运行时类型判断。联合类型可以减少接口中方法签名的数量,让 visit 方法接收一个联合类型参数:
interface Visitor
{
public function visit(ElementA|ElementB|ElementC $element): void;
}
class ConcreteVisitor implements Visitor
{
public function visit(ElementA|ElementB|ElementC $element): void
{
match (true) {
$element instanceof ElementA => $this->visitA($element),
$element instanceof ElementB => $this->visitB($element),
$element instanceof ElementC => $this->visitC($element),
default => throw new LogicException('Unknown element'),
};
}
}
这种做法并没有改变访问者模式的本质,仍然需要在实现中根据元素类型进行二次分发,但它让接口定义更简洁,分支逻辑集中在一个方法中。需要注意的是,联合类型不能表达“这个对象同时实现了 A 和 B”的交叉约束,也无法替代行为契约。如果联合类型成员超过三四个,或者经常发生变化,说明类型边界本身可能不够稳定,此时应当重新审视是否需要引入接口或抽象类。
此外,应避免使用 string|int|bool|null 这种含义模糊的宽泛联合。它虽然写起来方便,但会让类型提示失去约束力,和直接使用 mixed 差别不大。更好的做法是缩小联合范围,让类型本身就能表达业务含义。PHPStan、Psalm 等静态分析工具对联合类型的穷尽分支检查也能帮助减少遗漏。
PHP 8 的联合类型给传统设计模式带来了一种新的实现视角。它把“参数可能是多个不相关类型”这一常见情况从文档注释提升为可执行的类型约束,让策略、工厂、建造者等模式在封闭类型集合下写起来更直接。但它不会取代接口,因为行为契约和多态调用依然需要真正的抽象。判断标准可以很简单:如果调用方需要针对不同类型做分支,联合类型是合适的;如果调用方希望通过统一方法直接调度,接口仍然是最稳妥的选择。把联合类型和 match 表达式配合使用,可以在保证可读性的同时减少样板代码。
PHP 8Union Types设计模式修改时间:2026-09-19 16:11:47