Hyperf 的 server:watch 本质是 Swoole 下 Worker 进程滚动重启,非 FPM 替代;其快因跳过全量加载与重建,仅重启 Worker,耗时 1–3 秒;需正确配置 watch.dir、清理 runtime/container/ 并手动 init-proxy 才能确保注解等变更生效。

Hyperf 的 server:watch 不是“替换 FPM 重启”,而是绕过传统 PHP 生命周期,在 Swoole 常驻进程模型下实现文件变更 → worker 滚动重启。它无法让 FPM 变成热更新,只能帮你彻底离开 FPM。
为什么 FPM 重启慢,而 Hyperf watch 能快?
FPM 每次重启要加载全部 PHP 文件、重建 OPcache、重连数据库连接池、重新扫描注解——整个流程串行阻塞,12 秒起步很常见。Hyperf 的 server:watch 启动的是 Swoole 主从进程模型:Master 进程常驻,只负责监听文件变化;Worker 进程在变更触发时被逐个优雅终止并拉起新实例。老请求继续处理,新请求路由到新 Worker,启动耗时压缩到 1–3 秒(取决于 autoload 和 DI 初始化量)。
- 本质区别:FPM 是“全进程销毁+重建”,Hyperf watch 是“Worker 进程滚动替换”
-
server:watch默认只 reloadapp/和config/下的 PHP 文件,不碰vendor/—— 所以改了第三方包仍需手动重启 - 如果项目里用了
opcache.enable=1且opcache.validate_timestamps=0,反而会干扰server:watch,建议开发环境关掉 opcache 或设为validate_timestamps=1
watch.dir 配置漏加路径导致修改不生效
你改了 app/Domain/User/Service.php 却没反应?默认 watcher 只监控 app/ 和 config/ 两个目录,但 app/ 是个符号路径,实际扫描的是其下的子目录。若你的代码在 app/Domain,必须显式加入配置:
- 编辑
config/autoload/watcher.php,把'paths' => ['app/', 'config/']改成'paths' => [BASE_PATH . '/app/Domain', BASE_PATH . '/app/Http', 'config/'] - 路径必须用正斜杠,
app\Domain(Windows 风格)会被忽略 - 改完配置后,必须 kill 当前
server:watch进程再重跑命令,它不会自动 reload 自己的配置 - 检查是否生效:改一个已监听路径里的文件,看终端是否打印
File changed: xxx.php, reloading...
注解变更后路由 404 或 Bean 不刷新
加了个新 Controller,server:watch 重启了,但访问 404?这不是热更新失效,是注解缓存没重建。Hyperf 的 DI 容器和路由元数据都依赖 runtime/container/annotation/ 下生成的代理类,而 server:watch 只管重启进程,不管重建这些文件。
- 首次添加控制器、修改
scan.paths、或删过runtime/container/后,必须手动执行:php bin/hyperf.php di:init-proxy - 如果
SCAN_CACHEABLE=true(默认开启),但runtime/container/annotation/下为空,注解就静默失效,无任何报错 - 枚举类(
enum)上的 OpenAPI 注解(如#[OA\Property])热更新必然失败,这是已知限制 —— 此时停掉server:watch,改用php bin/hyperf.php server:start+kill -USR1 $(cat runtime/hyperf.pid)
runtime/container/ 缓存残留引发行为不一致
改了 #[Cacheable] 注解或模型字段类型,但缓存策略/验证逻辑没变?大概率是 runtime/container/ 下旧代理类还在被加载。
- 最稳妥清理方式:
rm -rf runtime/container/ && php bin/hyperf.php di:init-proxy - 不要只删
annotation/,proxy/和di/下的文件也得清,否则 DI 容器可能复用旧定义 - Docker 构建时注意:
.dockerignore不能过滤runtime/,否则容器内始终用的是构建时缓存 - PHPStorm 的 File Watcher 方案(调
reload.sh)容易残留runtime/container/,不如原生命令可控
真正卡住人的从来不是“怎么开热更新”,而是“改了哪几处才让热更新真正覆盖到你正在调试的那一行”。路径配错、注解没刷、缓存没清——三者叠在一起,看起来就像热更新完全失效。动手前先确认 ps aux | grep watch 看进程是否由 server:watch 启动,再查 runtime/container/annotation/ 是否有对应文件,最后 curl 一下接口看日志输出是否更新。顺序不能乱。


















