
前言如果你在代码里见过这样的东西说明已经到了该动多态的时候?php if ($channel alipay) { // 80 行支付宝逻辑 } elseif ($channel wechat) { // 90 行微信逻辑 } elseif ($channel unionpay) { // 70 行银联逻辑 }症状很明确新增一个渠道要动三处判断、参数组装、结果解析特例一多分支里就会长出if (!$isSandbox)这样的小分支单元测试也没法只测一个渠道。多态polymorphism就是用来消灭这类分支的把每种类型各自怎么做从调用方搬回类型自己身上。PHP 早就有接口和抽象类但真正让这套写法不啰嗦的语法是 PHP 8.0 补上的。本文讲三件事三种落地方式如何分工、PHP 8.0 的哪些新语法让多态写起来更轻、继承里那几个看起来一样其实不一样的坑。一、多态的三种落地方式各管一段PHP 里能承担多态职责的只有三样东西但它们的定位完全不同机制解决的问题能不能被实例化一个类能用几个interface接口定义能做什么的契约不能可以implements多个abstract class抽象类契约 共用实现模板方法不能只能extends一个trait纯代码复用不是多态—可以用多个判断标准很简单只关心调用方需要固定哪些方法→ 用接口。调用方写PaymentGateway $gateway不需要知道金额怎么算。多个实现之间有共同骨架比如签名 → 发送 → 验签的顺序一样只有签名算法不同→ 用抽象类把骨架写成final的模板方法把差异点留成abstract钩子方法。只想复用几行代码、没有是一个的语义关系 → 用 trait。但不要用它替代接口因为调用方无法用类型声明约束一个 trait。二、PHP 8.0 让多态轻下来的语法下面这些全是PHP 8.0 引入的写多态时几乎都用得上。语法作用在多态场景里的价值构造器属性提升__construct(public int $id)直接声明属性子类构造函数不用再写一堆$this-x $x;联合类型int\stringstatic返回类型方法可以声明返回static静态工厂 晚静态绑定的正确写法nullsafe 链式调用$a?-b?-c取值链上少写三层null判断match表达式带返回值的严格比较分支工厂方法里名字到类的映射命名参数timeout: 3参数多时调用可读性更好必须注意版本边界enum、readonly属性、never返回类型、交叉类型AB全是PHP 8.1的特性DNF类型(AB)|C是PHP 8.2的Property Hooks是PHP 8.4的。在标了 8.0 的项目里用它们部署环境一旦是 8.0 就直接语法错误。三、继承里最容易搞混的两组概念3.1 协变返回与逆变参数PHP 7.4 起子类覆写父类方法时返回类型可以更具体协变covariance父类返回Animal子类可以返回Dog。参数类型可以更宽泛逆变contravariance父类参数是Dog子类可以放宽成Animal。反过来做就是致命错误Declaration of X::m() must be compatible with Y::m()。记法出去的东西可以变具体进来的东西只能变宽松。比如父类是pick(Dog $d): Animal子类写成pick(Animal $d): Dog是合法的参数放宽、返回收紧反过来就报错。3.2self::与static::写法含义谁用self::编译期就绑定到写这行代码的那个类访问本类常量、调用本类私有方法static::运行期绑定到实际被调用的那个类晚静态绑定Late Static Binding工厂方法、static::class、子类可覆写的默认值只要方法可能被子类继承并期望跟着子类走就必须写static::。四、实战用多态替掉渠道分支下面是一份可以直接运行的完整示例最低版本PHP 8.0。它演示了抽象类做模板方法、接口做契约、match做映射。?php declare(strict_types1); /** * 支付网关策略 模板方法 * 最低版本PHP 8.0 * 构造器属性提升、联合类型、match、nullsafe、static 返回类型、throw 表达式均为 8.0 引入 * readonly 属性是 8.1、枚举是 8.1本文刻意不用 */ final class PaymentResult { public function __construct( public bool $success, public string $tradeNo , public string $message , ) {} } interface PaymentGateway { public function pay(string $orderNo, int $amountCents): PaymentResult; public function supports(string $channel): bool; public function channel(): string; } abstract class AbstractGateway implements PaymentGateway { public function __construct( protected string $appId, protected string $appSecret, ) {} /** 模板方法把流程固定死final 防止子类打乱顺序 */ final public function pay(string $orderNo, int $amountCents): PaymentResult { if ($amountCents 0) { return new PaymentResult(false, , 金额必须大于 0); } $params $this-buildParams($orderNo, $amountCents); $params[sign] $this-sign($params); try { $raw $this-transport($params); } catch (RuntimeException $e) { return new PaymentResult(false, , 网关请求失败: . $e-getMessage()); } return $this-parseResponse($raw); } // 子类必须实现的三个钩子 abstract protected function buildParams(string $orderNo, int $amountCents): array; abstract protected function sign(array $params): string; abstract protected function parseResponse(string $raw): PaymentResult; // 子类可以覆写也可以直接用 protected function transport(array $params): string { // 真实项目里换成 curl 调用这里用一个确定的实现保证示例可跑 return json_encode( [code 0, trade_no T . $params[order_no]], JSON_THROW_ON_ERROR ); } public function supports(string $channel): bool { return $channel $this-channel(); } } final class AlipayGateway extends AbstractGateway { public function channel(): string { return alipay; } protected function buildParams(string $orderNo, int $amountCents): array { return [ app_id $this-appId, method alipay.trade.create, order_no $orderNo, amount number_format($amountCents / 100, 2, ., ), // 支付宝按元传 ]; } protected function sign(array $params): string { ksort($params); // 示例用 md5 便于观察对接真实网关时请换成它要求的 HMAC / RSA 方案 return md5(http_build_query($params) . $this-appSecret); } protected function parseResponse(string $raw): PaymentResult { $data json_decode($raw, true, 512, JSON_THROW_ON_ERROR); $ok ($data[code] ?? -1) 0; return new PaymentResult($ok, $ok ? (string) ($data[trade_no] ?? ) : , $ok ? 支付成功 : 支付失败); } } final class WechatGateway extends AbstractGateway { public function channel(): string { return wechat; } protected function buildParams(string $orderNo, int $amountCents): array { return [ mch_id $this-appId, out_trade_no $orderNo, total_fee $amountCents, // 微信按分传 ]; } protected function sign(array $params): string { ksort($params); return strtoupper(hash_hmac(sha256, http_build_query($params), $this-appSecret)); } protected function parseResponse(string $raw): PaymentResult { $data json_decode($raw, true, 512, JSON_THROW_ON_ERROR); $ok ($data[code] ?? -1) 0; return new PaymentResult($ok, $ok ? (string) ($data[trade_no] ?? ) : , $ok ? 支付成功 : 支付失败); } } /** 调用方只依赖接口完全不知道具体实现是谁 */ final class PaymentService { /** var PaymentGateway[] */ private array $gateways; public function __construct(PaymentGateway ...$gateways) { $this-gateways $gateways; } public function gatewayFor(string $channel): PaymentGateway { foreach ($this-gateways as $gateway) { if ($gateway-supports($channel)) { return $gateway; } } throw new InvalidArgumentException(不支持的支付渠道: {$channel}); } public function pay(string $channel, string $orderNo, int $amountCents): PaymentResult { // 一行搞定不需要任何 if/else return $this-gatewayFor($channel)-pay($orderNo, $amountCents); } } /* ---------------- 使用示例 ---------------- */ $service new PaymentService( new AlipayGateway(app-001, secret-a), new WechatGateway(mch-002, secret-w), ); $result $service-pay(alipay, ORDER-2024-0001, 12800); printf(alipay: success%s trade_no%s msg%s\n, var_export($result-success, true), $result-tradeNo, $result-message); $result $service-pay(wechat, ORDER-2024-0002, 500); printf(wechat: success%s trade_no%s\n, var_export($result-success, true), $result-tradeNo); // match 做名字 → 类的映射match 用 严格比较 $channel wechat; $gatewayClass match ($channel) { alipay AlipayGateway::class, wechat WechatGateway::class, default throw new InvalidArgumentException(未知渠道: {$channel}), }; printf(映射结果: %s\n, $gatewayClass);输出的关键几行alipay: successtrue trade_noTORDER-2024-0001 msg支付成功 wechat: successtrue trade_noTORDER-2024-0002 映射结果: WechatGateway常见坑点1. 子类写了构造函数却忘了调用parent::__construct()❌ 错误写法?php final class WechatGateway extends AbstractGateway { private string $mchId; public function __construct(string $appId, string $secret, string $mchId) { $this-mchId $mchId; // 父类的 $appId / $appSecret 从没被赋值 } }✅ 正确写法?php final class WechatGateway extends AbstractGateway { public function __construct( string $appId, string $secret, private string $mchId, ) { parent::__construct($appId, $secret); // 关键字是 parent } }PHP 的构造函数不会自动调用父类构造函数和 Java 不同。漏掉之后父类属性处于未初始化状态PHP 8.0 下访问它会抛Error: Typed property must not be accessed before initialization——报错点离构造函数很远很难查。2. 覆写方法时把参数类型改窄了❌ 错误写法?php class Base { public function handle(object $ctx): void {} } class Child extends Base { public function handle(MyContext $ctx): void {} } // Fatal error: Declaration of Child::handle(MyContext $ctx) must be compatible with Base::handle(object $ctx)✅ 正确写法参数只能放宽逆变返回类型只能收紧协变。要么保持父类签名不变、在方法体里做类型检查要么把父类签名本身改宽。3. 静态工厂用了new self()❌ 错误写法?php abstract class GatewayFactory { public static function create(): self { return new self(); // 绑定到 GatewayFactory 本身子类调用仍拿到父类实例 } }✅ 正确写法?php abstract class GatewayFactory { public static function create(): static // static 返回类型是 PHP 8.0 引入的 { return new static(); // 跟随实际调用的子类 } }同时把方法体里的self::class也换成static::class否则描述信息里的类名依然是父类。4. 以为match会做松散比较❌ 错误写法?php $channel 1; // 从 URL query 里拿到的永远是字符串 $class match ($channel) { 1 AlipayGateway::class, // 永远匹配不上 default null, };✅ 正确写法match用的是字符串1不等于整数1。先把$channel统一成字符串或者干脆全部用字符串做分支键。另外注意match没有匹配且没有default时会抛UnhandledMatchError它继承自Error而不是Exception。5. 用instanceof替代多态❌ 错误写法?php public function pay(string $channel, string $orderNo, int $amount): PaymentResult { if ($this-gateway instanceof AlipayGateway) { return $this-gateway-alipayPay($orderNo, $amount); // 分支又长回来了 } // ... }✅ 正确写法让所有网关实现同一个pay()调用方直接用接口方法。instanceof只在确实需要针对某个具体实现做特殊处理的地方用比如给沙箱网关加日志而且那种逻辑应该尽量塞进实现类自己。6. trait 方法冲突不处理❌ 错误写法两个 trait 都有sign()直接use A, B;报Trait method sign has not been applied, because there are collisions。✅ 正确写法用insteadof选一个用as给另一个起别名?php trait AlipaySign { public function sign(array $p): string { return a; } } trait WechatSign { public function sign(array $p): string { return w; } } final class Mixed { use AlipaySign, WechatSign { AlipaySign::sign insteadof WechatSign; WechatSign::sign as wechatSign; // 保留另一个能力 } }7. 在 PHP 8.0 项目里用了 8.1 的语法❌ 错误写法public function __construct(public readonly string $appId) {}——readonly属性是 8.1 引入的。✅ 正确写法8.0 下只能用构造器属性提升不能加readonly。同理enum、never、new出现在参数默认值里8.1、array_is_list()8.1都不能用。写在本地能跑、部署到 8.0 就Parse error。8. 构造器属性提升的属性在类体里又声明了一遍❌ 错误写法?php final class WechatGateway extends AbstractGateway { private string $mchId; // 和下面提升的属性重名 public function __construct( string $appId, private string $mchId, ) { parent::__construct($appId, ); } }✅ 正确写法提升的属性不要在类体里再声明一次否则会直接报属性重复声明的致命错误编译期就拦下来。类型、可见性、默认值全部写在构造参数上。总结需求用什么关键点固定调用方依赖的方法集合interface调用方类型声明用接口不用具体类固定流程、只留差异点abstract classfinal模板方法钩子方法写成abstract protected复用实现但不表达是一个trait不能替代接口多 trait 冲突用insteadof/as让子类构造更短构造器属性提升8.0不要与类体声明重复静态工厂返回正确子类static返回类型 new static()8.0用static::不用self::覆写方法时改类型返回协变、参数逆变7.4出可变具体进只能变宽多态的价值不在用上了接口而在新增一种类型时不需要改调用方。检验方法很干脆把新增渠道这件事交给同事看他需要改几个文件——只新增一个类、不碰PaymentService才算数。