如何通过装饰器模式在PHP中构建一个可扩展的权限控制体系?
- 内容介绍
- 文章标签
- 相关推荐
本文共计896个文字,预计阅读时间需要4分钟。
硬编码权限判断(例如:
装饰器模式在这里的价值不是炫技,而是把「谁可以做什么」从「怎么做」里剥离开。你新增一个权限规则,只需要写一个新类,注册到链里,不碰原有逻辑。
用PHP实现权限装饰器链的关键结构
核心是让每个装饰器实现统一接口,并持有下一个处理者($next)。它不决定最终放行与否,只做自己的判断:通过就交给下一个,不通过就直接返回拒绝响应。
- 定义接口
PermissionChecker,含check($user, $resource, $action)方法 - 基础装饰器如
RoleBasedChecker检查角色,OwnershipChecker检查资源归属 - 组合时用构造函数传入
$next,例如:new OwnershipChecker(new RoleBasedChecker(new DefaultDenyChecker())) - 最末端必须是兜底装饰器(如
DefaultDenyChecker),避免漏判返回 null 导致静默放行
示例片段:
立即学习“PHP免费学习笔记(深入)”;
class OwnershipChecker implements PermissionChecker { private $next; public function __construct(PermissionChecker $next) { $this->next = $next; } public function check($user, $resource, $action) { if ($action === 'edit' && $resource->owner_id === $user->id) { return true; } return $this->next->check($user, $resource, $action); } }
装饰器顺序为什么直接影响权限语义
顺序不是随意的。比如把 RateLimitChecker 放在鉴权之前,会导致未登录用户也被限流;把 VIPFeatureChecker 放在 RoleBasedChecker 之后,才能确保只有已通过角色检查的用户才进入 VIP 判断。
- 推荐顺序:认证前置(如 token 解析)→ 基础角色/组权限 → 资源级动态权限(归属、状态、时间窗)→ 特殊策略(VIP、灰度、A/B)→ 默认拒绝
- 避免循环依赖:装饰器内部不能反向调用自身或上游装饰器
- 调试时可在每个
check()开头加日志,输出当前装饰器名和判断结果,快速定位卡在哪一环
实际集成到 Laravel 或原生路由时的坑
很多人卡在“怎么让装饰器链接入请求生命周期”。不是所有框架都支持中间件式链式调用,硬塞进控制器会失去装饰器意义。
- Laravel 场景:把装饰器链封装成自定义中间件,在
handle()中调用根装饰器的check(),失败则 throwAuthorizationException - 原生 PHP:在路由分发前统一拦截,用
$_SERVER['REQUEST_URI']和$_SERVER['REQUEST_METHOD']推导$resource和$action,避免每个路由手动传参 - 常见错误:
check()返回null而非布尔值,导致if ($result)判定为 false —— 必须严格返回true或false - 性能注意:不要在装饰器里做 N+1 查询,
$user和$resource应该由上层预加载好,装饰器只做逻辑判断
复杂点在于资源抽象——$resource 不能只是 ID,得是带元数据的对象(比如有 status、created_at、owner_id 的实例),否则所有权或状态类规则没法写。这点容易被忽略,等加第二个动态规则时才返工。
本文共计896个文字,预计阅读时间需要4分钟。
硬编码权限判断(例如:
装饰器模式在这里的价值不是炫技,而是把「谁可以做什么」从「怎么做」里剥离开。你新增一个权限规则,只需要写一个新类,注册到链里,不碰原有逻辑。
用PHP实现权限装饰器链的关键结构
核心是让每个装饰器实现统一接口,并持有下一个处理者($next)。它不决定最终放行与否,只做自己的判断:通过就交给下一个,不通过就直接返回拒绝响应。
- 定义接口
PermissionChecker,含check($user, $resource, $action)方法 - 基础装饰器如
RoleBasedChecker检查角色,OwnershipChecker检查资源归属 - 组合时用构造函数传入
$next,例如:new OwnershipChecker(new RoleBasedChecker(new DefaultDenyChecker())) - 最末端必须是兜底装饰器(如
DefaultDenyChecker),避免漏判返回 null 导致静默放行
示例片段:
立即学习“PHP免费学习笔记(深入)”;
class OwnershipChecker implements PermissionChecker { private $next; public function __construct(PermissionChecker $next) { $this->next = $next; } public function check($user, $resource, $action) { if ($action === 'edit' && $resource->owner_id === $user->id) { return true; } return $this->next->check($user, $resource, $action); } }
装饰器顺序为什么直接影响权限语义
顺序不是随意的。比如把 RateLimitChecker 放在鉴权之前,会导致未登录用户也被限流;把 VIPFeatureChecker 放在 RoleBasedChecker 之后,才能确保只有已通过角色检查的用户才进入 VIP 判断。
- 推荐顺序:认证前置(如 token 解析)→ 基础角色/组权限 → 资源级动态权限(归属、状态、时间窗)→ 特殊策略(VIP、灰度、A/B)→ 默认拒绝
- 避免循环依赖:装饰器内部不能反向调用自身或上游装饰器
- 调试时可在每个
check()开头加日志,输出当前装饰器名和判断结果,快速定位卡在哪一环
实际集成到 Laravel 或原生路由时的坑
很多人卡在“怎么让装饰器链接入请求生命周期”。不是所有框架都支持中间件式链式调用,硬塞进控制器会失去装饰器意义。
- Laravel 场景:把装饰器链封装成自定义中间件,在
handle()中调用根装饰器的check(),失败则 throwAuthorizationException - 原生 PHP:在路由分发前统一拦截,用
$_SERVER['REQUEST_URI']和$_SERVER['REQUEST_METHOD']推导$resource和$action,避免每个路由手动传参 - 常见错误:
check()返回null而非布尔值,导致if ($result)判定为 false —— 必须严格返回true或false - 性能注意:不要在装饰器里做 N+1 查询,
$user和$resource应该由上层预加载好,装饰器只做逻辑判断
复杂点在于资源抽象——$resource 不能只是 ID,得是带元数据的对象(比如有 status、created_at、owner_id 的实例),否则所有权或状态类规则没法写。这点容易被忽略,等加第二个动态规则时才返工。

