PHP 8.1+ & Hyperf 3.1 协程架构开发规范技能 (Hyperf Framework Mastery)
概述 (Overview)
本技能定义了基于 PHP 8.1+ 与 Hyperf 3.1(Swoole 协程引擎) 进行高性能后端微服务开发的专属规范、分层架构与工程红线。 聚焦于 “四层分层清晰、协程状态隔离、强类型 DTO 校验、防 N+1 查询与消费幂等闭环”,彻底剔除与框架无关的通用冗余规则,专注于 Hyperf 与 Swoole 协程环境的生产级工程实践。
1. 架构分层与职责边界 (Architecture Layers)
Hyperf 工程必须严格遵循 Controller → DTO → Service → Repository 四层分层体系,严禁跨层调用与职责错位:
graph TD
Client["客户端请求"] --> Controller["Controller (HTTP/RPC 传输控制层)<br/>• 路由映射与中间件调度<br/>• 触发 DTO 自动校验<br/>• 统一响应封装 (code/msg/data)"]
Controller --> DTO["DTO (数据传输契约对象)<br/>• 强类型入参绑定与范围校验<br/>• 严禁任何数据库/网络 I/O"]
Controller --> Service["Service (领域业务与事务层)<br/>• 业务用例编排与规则计算<br/>• 事务边界控制 (Db::transaction)<br/>• 隔离传输层,不向底层泄漏 Request/Response"]
Service --> Repository["Repository (数据持久化与缓存层)<br/>• 屏蔽 SQL / GORM / Redis 访问细节<br/>• 复杂多表联合聚合查询<br/>• 不负责 HTTP 状态码与响应组装"]
1.1 各层职责与禁止红线
| 架构分层 | 核心职责 | 🚨 严厉禁止红线 |
|---|---|---|
| Controller | 路由 Mapping 声明、鉴权中间件触发、调用 Service、统一输出结构 | 严禁直接编写 SQL 查询、Redis 缓存读写或核心业务逻辑编排 |
| DTO | 强类型数据绑定、字段类型转换、业务范围与格式校验规则 | 严禁执行任何数据库、Redis、HTTP、MQ 等网络或存储 I/O 操作 |
| Service | 业务用例编排、业务状态机推进、分布式锁协同、事务边界控制 | 严禁直接读取 HTTP Request 对象或处理前端展示格式 |
| Repository | 封装 Model 查询、SQL 优化、Redis 缓存维护与批量数据读写 | 严禁包含 HTTP 响应组装逻辑,严禁处理非存储相关的业务校验 |
1.2 PHP 8 Attribute 注解与路由注册
- 优先使用原生 Attribute 注解:
- 路由与元数据统一在 Controller 上声明
#[Controller]以及#[GetMapping]、#[PostMapping]等; - 严禁在注解声明的同时又在
config/routes.php中重复注册同一路由; config/routes.php仅保留极少数无法由注解静态表达的动态路由(须加注释说明)。
- 路由与元数据统一在 Controller 上声明
- 依赖注入规范:
- 默认强制使用构造函数类型声明注入(Constructor Injection),提升代码可测试性与静态类型推导;
- 避免无节制使用
#[Inject]属性注入。
- 普通业务类禁止滥用注解:
- 不需要由 DI 容器生命周期管理的普通实体(Domain Entity、Value Object、DTO、配置常数),严禁挂载无意义的框架扫描注解。
1.3 HTTP 路由与参数约定
- 禁止使用 URL Path 参数承载业务字段:
- 严禁设计形如
/devices/{id}、/users/{user_id}/status的动态路径路由;
- 严禁设计形如
- 读请求(GET)规范:
- 标识与查询筛选条件统一使用 Query 参数(如
/devices/detail?id=1、/users/list?status=active);
- 标识与查询筛选条件统一使用 Query 参数(如
- 写请求(POST / PUT / PATCH)规范:
- 业务字段与更新数据统一置于 请求 Body (JSON) 中;
- 路由采用稳定动作或资源入口(如
/devices/rename,device_id与new_name均由 Body 传递);
- DTO 强制防御性校验:
- DTO 必须对 Query 或 Body 中的参数进行严格类型约束、必填项与取值范围校验。
2. Swoole 协程常驻内存安全黄金红线 (Coroutine & State Safety)
Hyperf 运行在 Swoole 常驻内存模式下,传统的 PHP-FPM “请求结束自动销毁所有变量” 的心智模型在此完全失效。必须严防跨协程状态串染:
🚨 红线一:严禁在单例属性中存储请求级状态
- Controller、Service、Repository 在 Hyperf 容器中默认均为长生命周期的共享单例;
- 绝对禁止将用户 ID、请求参数、租户信息或私有数据存储在类的普通成员属性(
$this->userId)或静态属性中; - 必须强制使用
Hyperf\Context\Context进行协程上下文隔离:use Hyperf\Context\Context; // ❌ 致命错误:在单例类属性中保存用户状态,高并发下导致 A 用户的账单被 B 用户看到 class OrderService { private int $currentUserId; // 绝对禁止! } // ✅ 唯一正解:使用协程上下文 (协程销毁时自动隔离并清理) class OrderService { public function setCurrentUser(int $userId): void { Context::set('current_user_id', $userId); } public function getCurrentUser(): ?int { return Context::get('current_user_id'); } }
🚨 红线二:禁止使用传统 PHP 全局超全局变量
- 严禁使用
$_GET、$_POST、$_REQUEST、$_SESSION、$GLOBALS; - 所有输入必须通过 Hyperf 注入的
Hyperf\HttpServer\Contract\RequestInterface或 DTO 对象安全获取。
🚨 红线三:协程并发与异常防击垮 (Worker Safety)
- 在通过
Coroutine::create派生子协程时,子协程代码体内必须 100% 自包含try-catch (\Throwable $e); - 未捕获的子协程异常会直接导致底层 Swoole Worker 进程异常退出、在途所有请求被腰斩重置:
use Hyperf\Coroutine\Coroutine; use Hyperf\Contract\StdoutLoggerInterface; Coroutine::create(function () use ($logger) { try { // 异步执行外部通知或日志上报 $this->notifyExternalGateway(); } catch (\Throwable $e) { // 必须拦截兜底,严禁抛出到协程顶层 $logger->error(sprintf('子协程执行异常: %s, 堆栈: %s', $e->getMessage(), $e->getTraceAsString())); } });
3. 数据访问与性能规范 (Data Access & Query Optimization)
3.1 杜绝循环内查询 (N+1 Query 绝对红线)
- 严禁在
foreach/for循环体内执行 SQL 查询或 Redis 单条读取; - 必须在循环外部使用
whereIn或MGET批量拉取数据,并在内存中通过关联键(KeyBy/GroupBy)对齐:// ❌ 致命性能瓶颈:循环查询导致 100 次数据库往返 (N+1) foreach ($orders as $order) { $user = $this->userRepo->findById($order->user_id); } // ✅ 生产级正解:批量获取并在内存中对齐 $userIds = array_unique(array_column($orders, 'user_id')); $users = $this->userRepo->findListByIds($userIds); $userMap = []; foreach ($users as $user) { $userMap[$user->id] = $user; }
3.2 安全的数组与对象属性访问
- 在处理可能存在缺失或嵌套层级深的数据时,禁止直接裸写
$data['a']['b']['c'](极易引发Undefined array key警告); - 统一使用 Hyperf 官方内置工具助手:
- 安全读取:
data_get($target, 'profile.address.city', '默认值'); - 安全设值:
data_set($target, 'profile.status', 'active')。
- 安全读取:
3.3 数据库事务与长事务拆分
- 事务边界由 Service 控制,使用闭包自动提交/回滚:
use Hyperf\DbConnection\Db; Db::transaction(function () use ($orderId) { $this->orderRepo->updateStatus($orderId, OrderStatus::PAID); $this->accountRepo->debitBalance(...); }); - 大事务与数据迁移拆分:在批量更新或处理海量数据时,严禁在单一事务中执行超过 1000 行的锁定操作,必须使用
chunkById游标分批次独立提交,防止长事务锁表引发死锁与连接池耗尽。
3.4 Redis 规范
- 禁止全量扫描:严禁在生产环境执行
KEYS *;需要模糊匹配 Key 时必须使用SCAN游标分批迭代; - 连接池生命周期:严格使用 Hyperf 依赖注入容器管理的 Redis 客户端,确保协程连接池复用。
4. 异步队列与定时任务规范 (Async Queue & Crontab)
4.1 异步队列 (AsyncQueue / AMQP) 幂等与重试
- 至少一次投递与消费端幂等:
- 网络抖动或超时重发会导致消息被重复投递,消费者逻辑必须以唯一业务编号(如
order_no、msg_id)为幂等键,消费前先检查状态或通过 Redis 防重锁拦截;
- 网络抖动或超时重发会导致消息被重复投递,消费者逻辑必须以唯一业务编号(如
- 重试上限与指数退避:
- 消息处理失败时,必须配置重试次数上限(如 3 次)与递增延迟时间(如 5s -> 30s -> 60s);
- 严禁对由于非法参数、业务规则拒绝等不可恢复的错误进行无限重试;达到上限必须入死信队列或记录告警日志。
4.2 定时任务多实例并发防护 (Distributed Crontab Lock)
- 在容器化、多 Pod/多实例集群部署环境下,Hyperf 的 Crontab 默认会在每个节点并发触发;
- 定时任务必须依赖分布式锁防重:
use Hyperf\Crontab\Annotation\Crontab; use Hyperf\Redis\Redis; #[Crontab(name: "DailyReconcile", rule: "0 2 * * *", memo: "每日对账任务")] public function execute(): void { $lockKey = "crontab:lock:daily_reconcile:" . date('Ymd'); // 申请 1 小时有效期的分布式锁 (NX + EX) $acquired = $this->redis->set($lockKey, 1, ['NX', 'EX' => 3600]); if (!$acquired) { return; // 已被其他 Pod 实例抢占执行,安全跳过 } $this->reconcileService->run(); }
5. PHP 8.1+ 地道编码风格与类型安全
- 强制严格类型:所有 PHP 新文件头部必须显式声明:
<?php declare(strict_types=1); - 杜绝无约束裸数组,拥抱强类型:
- 方法参数与返回值必须显式标注强类型,善用 PHP 8.1+
readonly属性、联合类型(int|string)与枚举类(enum OrderStatus: string); - 严禁把结构复杂的实体以未声明任何键的裸关联数组长距离透传。
- 方法参数与返回值必须显式标注强类型,善用 PHP 8.1+
- 异常处理原则:
- 严禁空
catch (\Throwable $e) {}吞掉异常; - 异常日志必须包含 TraceID 与业务关键参数,便于链路追踪。
- 严禁空
6. Hyperf 规范审查 Checklist
- 四层分层:Controller 是否未包含 SQL 读写?Service 是否未混入 HTTP 传输对象?
- 协程状态安全:是否有请求级状态保存在了单例类的成员属性中?是否使用了
Context::get/set? - 子协程防御:派生的异步子协程是否已用
try-catch (\Throwable $e)全局捕获? - 路由传参约定:接口是否避免了 URL Path 传递业务字段?GET 是否用 Query?写接口是否用 Body?
- N+1 查询排查:循环体内是否不存在任何 SQL 或 Redis 单条调用?
- 队列幂等防重:消费者是否针对重复消息具备幂等保障?重试是否有明确上限?
- 定时任务加锁:Crontab 是否已添加基于 Redis 的分布式互斥锁防止多节点并发重复执行?