Hyperf 2.0 升级指南:Skeleton 项目从 1.1 平滑迁移的完整实操手册
Hyperf 2.0 升级指南:Skeleton 项目从 1.1 平滑迁移的完整实操手册
本指南以 Hyperf 官方 2.0 升级文档为主体,系统梳理从 1.1 版本迁移到 2.0 的每一个关键改动点:从 Swoole 版本要求、AOP 底层逻辑变更带来的入口文件与 composer.json 调整,到注解扫描缓存、异常处理、gRPC 客户端命名等配置项的逐项核对。读完本文,你将能独立完成一个基于官方 Skeleton 的 1.1 项目的 2.0 升级,并理解每项改动背后的源码原理,从而举一反三地处理自己重写过的框架内部文件。
升级前须知:版本路径与升级顺序
2.0 版本新增了大量能力,但升级动作主要围绕 Skeleton 项目的配置与依赖展开。请先根据自身情况确认升级路径:
- 基于官方 Skeleton 创建的 1.1 应用项目:按照本文后续各节逐项调整即可完成迁移;
- 全新项目:直接通过
composer create-project hyperf/hyperf-skeleton创建新项目,即可获得 2.0 版本的 skeleton 代码,无需执行本文的迁移步骤; - 版本低于 1.1 的项目:必须先升级到 1.1,再依据本文指南升级到 2.0,不可跨版本直接迁移。
升级 Swoole 到 4.5+
2.0 版本将最低 Swoole 版本要求从 4.4+ 提升到了 4.5+。4.4 与 4.5 之间在使用上存在一些细节差异,Hyperf 在较早版本便已适配了这些差异,因此业务代码无需理会,升级 Swoole 到 4.5+ 主要是减少历史包袱对 Hyperf 造成的长期影响。
可通过以下命令查看当前环境中的 Swoole 版本:
php --ri swoole
升级 Swoole 时,参考 Swoole 官方文档中关于环境部署的指引完成版本升级即可。升级完成后,可通过再次执行 php --ri swoole 确认版本号不低于 4.5。
入口文件添加 ClassLoader 初始化
2.0 改变了 AOP 的底层逻辑,注解扫描与代理类生成的时机前移,因此需要在框架入口文件 bin/hyperf.php 的匿名函数第一行添加初始化代码 Hyperf\Di\ClassLoader::init();,完整入口文件如下:
<?php
ini_set('display_errors', 'on');
ini_set('display_startup_errors', 'on');
error_reporting(E_ALL);
date_default_timezone_set('Asia/Shanghai');
! defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));
! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', SWOOLE_HOOK_ALL);
require BASE_PATH . '/vendor/autoload.php';
// Self-called anonymous function that creates its own scope and keep the global namespace clean.
(function () {
Hyperf\Di\ClassLoader::init();
/** @var \Psr\Container\ContainerInterface $container */
$container = require BASE_PATH . '/config/container.php';
$application = $container->get(\Hyperf\Contract\ApplicationInterface::class);
$application->run();
})();
从源码看,Hyperf\Di\ClassLoader::init()(src/di/src/ClassLoader.php)承担了启动早期最关键的三件事:
- 解析注解扫描配置:通过
ScanConfig::instance($configDir)读取config/autoload/annotations.php、config/config.php等配置,构建反射类映射并注册到 Composer 自动加载器; - 扫描并生成代理类:使用
Scanner对全量类进行注解扫描,将生成的代理类映射写入默认代理目录BASE_PATH/runtime/container/proxy/,并注册进自动加载器; - 初始化懒加载:调用
LazyLoader::bootstrap($configDir),将 LazyLoader 预置到自动加载队列顶部,为 DI 懒加载功能做好准备。
这正是后文"调整 composer.json"中要清理 runtime/container 目录的原因——代理类缓存需要随代码变更而失效重建。
与此同时,PHPUnit 的入口文件也需要做同样的处理,文件位于 tests/bootstrap.php:
<?php
declare(strict_types=1);
error_reporting(E_ALL);
date_default_timezone_set('Asia/Shanghai');
! defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));
! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', SWOOLE_HOOK_ALL);
Swoole\Runtime::enableCoroutine(true);
require BASE_PATH . '/vendor/autoload.php';
Hyperf\Di\ClassLoader::init();
$container = require BASE_PATH . '/config/container.php';
$container->get(Hyperf\Contract\ApplicationInterface::class);
注意测试入口中 Swoole\Runtime::enableCoroutine(true) 会先启用协程运行时,再执行 ClassLoader::init(),保证测试环境下注解扫描与代理生成逻辑与生产一致。
调整 composer.json
由于 2.0 版本 AOP 底层逻辑的调整,原本负责生成代理的 init-proxy.sh 脚本已被移除,因此需要:
- 从
composer.json的scripts.post-autoload-dump中删除"init-proxy.sh"执行语句; - 将
post-autoload-dump内的命令改为rm -rf runtime/container,即每次依赖安装/转储后清空代理缓存目录。
调整后的完整示例(不含无关字段)如下:
{
"scripts": {
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-autoload-dump": [
"rm -rf runtime/container"
],
"analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config",
"cs-fix": "php-cs-fixer fix $1",
"start": "php ./bin/hyperf.php start",
"test": "co-phpunit -c phpunit.xml --colors=always"
}
}
调整 composer.json 的依赖版本
原 Skeleton 项目默认依赖 1.1.x 版本的组件,升级到 2.0 需要调整依赖约束:
- 将所有 Hyperf 组件的依赖约束
~1.1.0修改为~2.0.0; - 将
phpstan/phpstan版本依赖修改为^0.12。
修改完成后运行 composer update 将依赖项升级到 2.0 版本。这一步与上一节的 post-autoload-dump 脚本联动:依赖更新后自动清理 runtime/container,确保新版本的代理类在下次启动时重新生成。
调整 Dockerfile:生产环境预热代理与扫描缓存
在 Docker 镜像打包过程中主动执行一次 php bin/hyperf.php,会提前创建所有需要生成的代理类和注解扫描缓存,这样生产环境运行时便无需再次扫描,可极大优化生产启动时间与内存使用量。以下示例仅包含本次需要修改的 Dockerfile 片段:
ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
APP_ENV=prod \
SCAN_CACHEABLE=(true)
COPY . /opt/www
RUN composer install --no-dev -o && php bin/hyperf.php
EXPOSE 9501
ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"]
关键点说明:
APP_ENV=prod与SCAN_CACHEABLE=(true)共同开启生产环境下的扫描缓存(其具体读取逻辑见下一节);RUN composer install --no-dev -o && php bin/hyperf.php在镜像构建阶段完成依赖安装与代理/缓存预热,构建产物中已包含runtime/container下的代理类,运行时不再扫描;- 注意容器内工作目录为
/opt/www,因此入口命令使用/opt/www/bin/hyperf.php。
非 Docker 部署的用户,在重新启动服务之前,最好先执行一次 php bin/hyperf.php 再启动服务,以预热代理类和缓存,减少重启耗时。
调整 config/config.php:app_env 与 scan_cacheable
需要在 config/config.php 配置中添加 app_env 和 scan_cacheable 两个配置项:
<?php
return [
// 生产环境使用 prod 值
'app_env' => env('APP_ENV', 'dev'),
// 是否使用注解扫描缓存
'scan_cacheable' => env('SCAN_CACHEABLE', false),
];
scan_cacheable 用于控制应用启动时是否使用注解扫描缓存。当值为 true 时,项目启动时会认为所有类都已完成扫描并正确生成了对应的缓存和代理,从而跳过扫描阶段,以优化启动时间和减少内存开销。
结合源码可以更准确地理解这两个配置项的语义。在 src/di/src/Annotation/ScanConfig.php 的 initConfigByFile 方法中,配置读取逻辑为:
$appEnv = $configContent['app_env'] ?? 'dev';
$cacheable = value($configContent['scan_cacheable'] ?? $appEnv === 'prod');
也就是说:当 scan_cacheable 未被显式配置时,只要 app_env 为 prod,扫描缓存也会默认启用。这正是上一节 Dockerfile 中同时设置 APP_ENV=prod 与 SCAN_CACHEABLE=(true) 的原因——二者协同,确保生产镜像在构建期即完成缓存预热,运行期直接跳过扫描。开发环境(app_env=dev)下默认不启用缓存,以保证代码改动后注解与代理能被重新扫描。
修改 config/autoload/logger.php:对齐 Monolog 日志格式
2.0 版本提高了对 Monolog 依赖的版本,高版本 Monolog 的默认日志格式发生了变化。如果对日志格式有要求(例如需要根据日志格式与日志系统对接),可修改 config/autoload/logger.php 配置文件的 dateFormat 配置项,以保持与之前版本一致:
<?php
declare(strict_types=1);
return [
'default' => [
'handler' => [
'class' => Monolog\Handler\StreamHandler::class,
'constructor' => [
'stream' => BASE_PATH . '/runtime/logs/hyperf.log',
'level' => Monolog\Logger::DEBUG,
],
],
'formatter' => [
'class' => Monolog\Formatter\LineFormatter::class,
'constructor' => [
'format' => null,
'dateFormat' => 'Y-m-d H:i:s',
'allowInlineLineBreaks' => true,
],
],
'processors' => [
],
],
];
核心是 formatter.constructor.dateFormat 设为 'Y-m-d H:i:s'(即 Y-m-d H:i:s),可保持与旧版一致的日志时间戳格式;format => null 表示使用 LineFormatter 的默认行格式,allowInlineLineBreaks => true 允许日志消息内包含换行。若你的项目仅关心日志内容而不依赖具体时间格式,此项可保持不变。
修改 config/autoload/exceptions.php:统一 HTTP 路由异常处理
2.0 版本对路由找不到(404)、请求方法不匹配(405) 等 HTTP 路由异常行为的处理逻辑进行了调整,统一改为抛出 Hyperf\HttpMessage\Exception\HttpException 的子异常类,再通过 ExceptionHandler 统一管理并响应。用户也可以直接抛出对应异常以获得一致的响应返回体验。
从源码看,HttpException 本身继承自 RuntimeException,其子类家族覆盖了常见 HTTP 错误语义,例如:
NotFoundHttpException(404,路由找不到,见 src/http-message/src/Exception/NotFoundHttpException.php);MethodNotAllowedHttpException(405,请求方法不匹配,见 src/http-message/src/Exception/MethodNotAllowedHttpException.php);BadRequestHttpException、ForbiddenHttpException、UnauthorizedHttpException、ServerErrorHttpException等,均位于 src/http-message/src/Exception/ 目录。
而 2.0 提供的 Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler(src/http-server/src/Exception/Handler/HttpExceptionHandler.php)是这套异常机制的标准处理器:其 isValid() 方法判断异常是否为 HttpException 实例,handle() 方法则停止异常传播链,并将异常的状态码与消息直接写入响应。由于 ExceptionHandler 是用户管理的机制,而 1.1 版本默认 Skeleton 配置了 App\Exception\Handler\AppExceptionHandler 对异常进行托底并统一以 500 状态码返回,因此需要将 HttpExceptionHandler 配置到 config/autoload/exceptions.php 中,并确保其位于 App\Exception\Handler\AppExceptionHandler 之前(先匹配先处理):
<?php
return [
'handler' => [
'http' => [
Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class,
],
],
];
完成配置后,可通过直接访问一个不存在的路由来验证:如果响应的状态码为 404,即表示此项配置修改成功。
修改 gRPC 客户端:基类方法命名调整
为了避免与 gRPC 实际业务的命名冲突,2.0 版本对 gRPC 客户端的基类函数命名进行了调整,原有公开方法统一加上下划线前缀:
| 原方法名 | 2.0 新方法名 |
|---|---|
simpleRequest |
_simpleRequest |
clientStreamRequest |
_clientStreamRequest |
getGrpcClient |
_getGrpcClient |
除此之外,一些不应暴露的方法变成了私有方法。如果你的 gRPC 客户端代码涉及以上方法的调用,请同步进行命名调整。相关组件实现位于 src/grpc-client/(客户端)与 src/grpc-server/(服务端),升级时可用 IDE 的全局搜索定位所有旧方法名的调用点逐一替换。
移除 DI 懒加载监听器
如果项目中使用到了 DI 组件的懒加载功能,此前需要注册一个 Hyperf\Di\Listener\LazyLoaderBootApplicationListener 监听器;在 2.0 版本中该监听器已被移除,可以直接使用该功能。因此,如果此前使用过该功能,需要在 config/autoload/listeners.php 中移除该监听器的注册。
结合前文可知,懒加载的初始化已内置到 ClassLoader::init() 的 LazyLoader::bootstrap() 调用中(实现见 src/di/src/LazyLoader/LazyLoader.php),无需再通过监听器在应用启动阶段单独引导,这也是该监听器被移除的底层原因。
绑定 NormalizerInterface 关系
当使用了 JSONRPC 功能并借助 symfony/serializer 库提供序列化功能时,由于 2.0 版本不再自动映射 Hyperf\Contract\NormalizerInterface 的实现类,需要手动添加该映射关系(写入依赖注入配置,如 config/autoload/dependencies.php):
use Hyperf\Utils\Serializer\SerializerFactory;
use Hyperf\Utils\Serializer\Serializer;
return [
Hyperf\Contract\NormalizerInterface::class => new SerializerFactory(Serializer::class),
];
即将 NormalizerInterface 显式绑定到 SerializerFactory 工厂,由其基于 Serializer::class 创建序列化器实例。需要说明的是,2.0 时代 Hyperf 的序列化相关实现后续演进为独立的 src/serializer/ 组件,Hyperf\Utils\Serializer 中的类在后续版本可能发生位置调整,升级后若遇到类找不到,请以当前组件目录中的实际类名为准调整命名空间。
调整 Hyperf\Contract\ProcessInterface 的实现类
Hyperf\Contract\ProcessInterface 中的 isEnable 方法增加了一个 $server 参数,签名由无参变为 isEnable($server): bool。从 src/contract/src/ProcessInterface.php 的接口定义可以看到完整的方法集:
bind($server): void:根据进程编号创建进程对象并绑定到 Server;isEnable($server): bool:2.0 新增$server参数,用于决定进程是否启动;handle(): void:进程的实际业务逻辑入口。
所有 ProcessInterface 的实现类(包括自定义常驻进程、定时任务进程等)都需要对 isEnable 方法补充 $server 参数声明,否则将因不符合接口签名而报错。升级时可全局搜索 implements ProcessInterface 定位所有实现类逐一调整。
检查 config/autoload/aspects.php 文件
如果你此前对 Skeleton 进行过精简操作,需要检查 config/autoload/aspects.php 文件是否存在;如不存在,需要新增该文件并返回一个空数组:
<?php
return [];
该问题在 2.0.1 版本会被修正,后续可无需做此检查。
检查自定义注解的收集器
如果使用了自定义注解,且使用自定义收集器 Collector 来收集注解元数据,则需要将对应的 Collector 配置到 annotations.scan.collectors 中。原因在于:开发模式下,框架会根据文件的修改时间判断文件是否修改,然后决定是否重新收集对应的注解元数据。因此,当没有配置 annotations.scan.collectors 时,注解只会在首次启动 server 时生效,后续修改将不会触发重新收集。
在应用层,该配置位于 config/autoload/annotations.php 文件:
<?php
return [
'scan' => [
'collectors' => [
CustomCollector::class,
],
],
];
在组件中,该配置则由 ConfigProvider 提供,结构相同:
<?php
return [
'annotations' => [
'scan' => [
'collectors' => [
CustomCollector::class,
],
],
]
];
从 src/di/src/Annotation/ScanConfig.php 的配置加载流程(initConfigByFile)可以看到,annotations 配置会优先从各组件 ConfigProvider 汇总,再与 config/autoload/annotations.php 合并——这正是应用层与组件层两种配置方式能够共存并最终生效的机制。若你的业务自定义注解在升级后出现"改代码不生效"的诡异现象,优先排查此配置项。
启动服务并测试访问接口
使用 Swoole 4.5 版本与 view 组件时,如果出现接口 404 的问题,可以尝试删除 config/autoload/server.php 文件中的 static_handler_locations 配置项。
原因:static_handler_locations 配置下的路径都会被框架认为是静态文件路由;如果配置了 /,就会导致所有接口都被认为是文件路径,从而全部返回 404。因此,若你的项目并不需要静态文件处理(或静态目录与接口路由存在冲突),删除该项即可恢复接口访问。
完成升级:遗留事项与问题反馈
至此,2.0 版本升级即已完成。但由于 Hyperf 的各个底层文件都可以通过 DI 实现重写,如果你重写了某些本次升级调整到的框架内部文件(例如自定义的 Process 进程类、异常处理器、序列化相关绑定等),仍需根据你的实际情况进行相应调整。
如你在升级过程中或升级后遇到任何问题,请前往 Hyperf 的 GitHub Issue 提交 issue,说明遇到的问题,社区会尽快协助解决。