代理模式在 PHP 中应实现与真实对象相同的接口,通过显式委托而非 __call() 实现,注入 AccessStrategy 策略控制权限,并严格透传 null、异常、引用参数及序列化行为。

代理模式在 PHP 中怎么写才不变成“套娃”
PHP 本身没有内置的 Proxy 类或语言级代理语法,所有代理都得手动实现——不是靠魔术方法自动转发,而是靠显式委托。很多人一上来就猛写 __call(),结果真实对象的类型信息全丢了,IDE 提示失效,类型检查形同虚设。
正确做法是让代理类**实现与真实对象相同的接口**。这样调用方完全无感,静态分析、IDE 补全、类型提示全在线。
- 必须定义一个接口(比如
PaymentProcessor),真实类和代理类都implements它 - 代理类构造时接收真实对象实例,仅作持有,不继承它
- 每个方法里做权限/日志/缓存等逻辑,再显式调用真实对象对应方法
- 避免在代理里重写真实对象未声明在接口中的方法,否则会破坏契约
怎么在代理里控制访问权限而不硬编码判断逻辑
权限不该写死在代理方法里,比如 if ($user->role !== 'admin') throw new Exception(...) —— 这会让代理类快速膨胀且无法复用。
真正可维护的做法是把权限决策抽成独立策略,并通过构造参数注入。代理只负责“执行策略 + 转发”,不关心“谁有权限”。
立即学习“PHP免费学习笔记(深入)”;
- 定义策略接口
AccessStrategy,实现类如RoleBasedStrategy或TokenScopeStrategy - 代理类构造函数接收
AccessStrategy实例和真实对象实例 - 在每个需鉴权的方法开头调用
$this->strategy->canAccess($method, $args) - 策略返回
false时直接抛出AccessDeniedException,不往下走
这样换权限规则只需换策略实例,代理类一行不用动。
__call() 和接口代理哪个更靠谱
用 __call() 实现通用代理看似省事,实际埋雷:类型系统失能、IDE 失去跳转能力、参数类型丢失、无法静态分析——尤其当你代理的是带类型声明的方法(如 process(string $id): Result)时,__call() 返回值只能是 mixed。
-
__call()适合调试代理、日志代理等“不暴露给业务代码”的内部工具 - 面向业务层的访问控制代理,必须走接口实现 + 显式方法委托
- PHP 8.2+ 的
__invoke()或只读属性对代理没帮助,别凑热闹 - 如果真实类方法太多,用 IDE 自动生成代理方法(PHPStorm 支持 “Generate Delegation Methods”)比手写安全
真实项目里最容易被忽略的三个点
代理不是加个壳就完事。下面三点漏掉一个,上线后就容易出 silent fail:
- 真实对象可能返回
null或抛出异常,代理必须原样透传,不能吞掉或转成其他异常类型 - 如果真实对象方法接受引用参数(
&$data),代理方法签名也必须带&,否则引用失效 - 代理类的
serialize()/unserialize()行为要和真实对象一致,否则放进 Session 或 Redis 会出问题
尤其是引用参数和序列化,90% 的代理 bug 都出在这俩地方,但没人查。











