618年中限时 5折! 详情

hyperf 框架完善之异常处理

956 0 0

上一节课我们封装了统一响应的方法,敲过一遍代码的小伙伴可能会发现一个明显的问题,对于业务逻辑的处理,我们往往会写在 Service 中,比如像下面这样:

App\Service\IndexService (自行创建)如下:

<?php
declare(strict_types=1);
namespace App\Service;

class IndexService
{
    public function info(int $id)
    {
        if ($id <= 0) {
            throw new \Exception("id无效");
        }
        return ['info' => 'data info'];
    }
}

这个时候就需要我们在 IndexController::info 方法内通过 try/catch 捕获处理了,如下:

use App\Service\IndexService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\AutoController;

#[AutoController]
class IndexController extends AbstractController
{
    /**
     * @var IndexService
     */
    #[Inject]
    public $indexService;

    public function info()
    {
        try {
            return $this->response->success($this->indexService->info($id));
        } catch (\Throwable $e) {
            return $this->response->fail(500, $e->getMessage());
        }
    }
}
其中,IndexService 是通过 `#[Inject]` 注解注入由 `@var` 注解声明的属性类型对象。依赖注入参考 https://hyperf.wiki/3.0/#/zh-cn/di?id=%e9%80%9a%e8%bf%87-inject-%e6%b3%a8%e8%a7%a3%e6%b3%a8%e5%85%a5

这样看似解决了主动抛异常的问题,但是也带来了新的问题,比如每个方法都要用 try/catch 捕获,无疑增加了很多麻烦。

hyperf 提供了异常处理器(ExceptionHandler),专门处理业务流程中没有捕获的异常。

异常处理器需要在 config/autoload/exceptions.php 文件内配置。hyperf 框架默认配置了两个 ErrorHandle,如下:

return [
    'handler' => [
        'http' => [
            Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class,
            App\Exception\Handler\AppExceptionHandler::class,
        ],
    ],
];

我们找到 Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler 的 isValid 方法,会发现它针对的是 Hyperf\HttpMessage\Exception\HttpException 的异常处理。

public function isValid(Throwable $throwable): bool
{
    return $throwable instanceof HttpException;
}

处理的方式(参考 Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::handle)也很简单,看下面3行代码

public function handle(Throwable $throwable, ResponseInterface $response)
{
    // ①
    $this->logger->debug($this->formatter->format($throwable));
    // ②
    $this->stopPropagation();
    // ③
    return $response->withStatus($throwable->getStatusCode())->withBody(new SwooleStream($throwable->getMessage()));
}
  • ①、控制台输出异常信息
  • ②、异常处理到此为止,不再继续冒泡传递
  • ③、http 直接响应 http 状态码并输出异常信息
  • curl 请求一个不存在的路由测试一下

    ➜  ~ curl -I http://127.0.0.1:9501/index/info222
    HTTP/1.1 404 Not Found
    Server: swoole-http-server
    Connection: keep-alive
    Content-Type: text/html
    Date: Tue, 01 Nov 2022 08:40:10 GMT
    

    Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler 是 hyperf 的源码文件,我们不好修改,先直接屏蔽掉,等下在 AppExceptionHandler 内统一处理。

    现在我们只保留了一个 App\Exception\Handler,看下 AppExceptionHandler::handle 方法,也是向控制台输出错误信息、异常栈,并输出 Internal Server Error。这对我们也没什么意义,改写 AppExceptionHandler 之前,先创建一个业务异常类,后续主动抛异常主要使用这个类即可。

    app/Exception 目录新增 BusinessException,继承 Hyperf\Server\Exception\ServerException,代码如下:

    <?php
    declare(strict_types=1);
    
    namespace App\Exception;
    use Hyperf\Server\Exception\ServerException;
    
    class BusinessException extends ServerException
    {
    }
    

    AppExceptionHandler 目前为止,我们需要针对 App\Exception\BusinessException、Hyperf\HttpMessage\Exception\HttpException 单独捕获并处理,如下:

    <?php
    
    declare(strict_types=1);
    
    namespace App\Exception\Handler;
    
    use App\Components\Response;
    use App\Exception\BusinessException;
    use Hyperf\ExceptionHandler\ExceptionHandler;
    use Hyperf\ExceptionHandler\Formatter\FormatterInterface;
    use Hyperf\HttpMessage\Exception\HttpException;
    use Hyperf\Logger\LoggerFactory;
    use Hyperf\Utils\ApplicationContext;
    use Psr\Container\ContainerInterface;
    use Psr\Http\Message\ResponseInterface;
    use Throwable;
    
    class AppExceptionHandler extends ExceptionHandler
    {
        protected $logger;
    
        /**
         * @var Response
         */
        protected $response;
    
        public function __construct(ContainerInterface $container, Response $response)
        {
            $this->logger = $container->get(LoggerFactory::class)->get('exception');
            $this->response = $response;
        }
    
        public function handle(Throwable $throwable, ResponseInterface $response)
        {
            $formatter = ApplicationContext::getContainer()->get(FormatterInterface::class);
            // 业务异常类
            if ($throwable instanceof BusinessException) {
                return $this->response->fail($throwable->getCode(), $throwable->getMessage());
            }
    
            // HttpException
            if ($throwable instanceof HttpException) {
                return $this->response->fail($throwable->getStatusCode(), $throwable->getMessage());
            }
    
            $this->logger->error($formatter->format($throwable));
    
            return $this->response->fail(500, env('APP_ENV') == 'dev' ? $throwable->getMessage() : 'Server Error');
        }
    
        public function isValid(Throwable $throwable): bool
        {
            return true;
        }
    }
    

    isValid 方法返回 true,表示接收所有异常并处理,在处理方法 handle 中,除了 App\Exception\BusinessException、Hyperf\HttpMessage\Exception\HttpException 异常,其他均通过 App\Components\Response::fail 方法处理并把所有异常记录到日志文件。

    现在 IndexController::info 方法改写回来如下:

    public function info()
    {
        $id = (int) $this->request->input('id', 0);
    
        return $this->response->success($this->indexService->info($id));
    }
    

    IndexService::info 改写如下:

    use App\Exception\BusinessException;
    
    class IndexService
    {
        public function info(int $id)
        {
            if ($id <= 0) {
                throw new BusinessException("id无效");
            }
    
            return ['info' => 'data info'];
        }
    }
    

    curl测试下:

    ➜  ~ curl http://127.0.0.1:9501/index/info\?id\=0
    {"code":500,"message":"id无效"}%
    
    ➜  ~ curl http://127.0.0.1:9501/index/info222
    {"code":404,"message":"Not Found"}%
    

    到这里,我们的框架已经初现雏形了,但我们的论坛准备面向全世界的用户,像上面这样抛出一个“id无效”,老外还以为是乱码了。

    下一节我们让框架支持国际化。

    立即购买