MineAdmin 交流群

官方QQ群: 150105478

Skip to content

錯誤處理

預設的異常處理機制

TIP

要理解 MineAdmin 的異常處理,首先需要你對 Hyperf 的錯誤處理有一定的瞭解。 本文不講述基礎性的說明

config/autolaod/exceptions.php 中,預設自帶了一些異常處理器. AppExceptionHandler 是對整個應用程式最終處理措施。如非必要請勿修改順序邏輯

php
<?php

declare(strict_types=1);
/**
 * This file is part of MineAdmin.
 *
 * @link     https://www.mineadmin.com
 * @document https://doc.mineadmin.com
 * @contact  root@imoi.cn
 * @license  https://github.com/mineadmin/MineAdmin/blob/master/LICENSE
 */
use App\Exception\Handler\AppExceptionHandler;
use App\Exception\Handler\BusinessExceptionHandler;
use App\Exception\Handler\JwtExceptionHandler;
use App\Exception\Handler\UnauthorizedExceptionHandler;
use App\Exception\Handler\ValidationExceptionHandler;
use Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler;
use Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler;

return [
    'handler' => [
        'http' => [
            // 處理業務異常
            BusinessExceptionHandler::class,
            // 處理未授權異常
            UnauthorizedExceptionHandler::class,
            // 處理驗證器異常
            ValidationExceptionHandler::class,
            // 處理JWT異常
            JwtExceptionHandler::class,
            // 處理應用異常
            AppExceptionHandler::class,
        ],
    ],
];
php
final class AppExceptionHandler extends AbstractHandler
{
    public function handleResponse(\Throwable $throwable): Result
    {
        $this->stopPropagation();
        return new Result(
            code: ResultCode::FAIL,
            message: $throwable->getMessage()
        );
    }
    
    public function isValid(\Throwable $throwable): bool
    {
        return true;
    }
}
php

<?php

declare(strict_types=1);
/**
 * This file is part of MineAdmin.
 *
 * @link     https://www.mineadmin.com
 * @document https://doc.mineadmin.com
 * @contact  root@imoi.cn
 * @license  https://github.com/mineadmin/MineAdmin/blob/master/LICENSE
 */

namespace App\Exception\Handler;

use App\Http\Common\Result;
use Hyperf\Codec\Json;
use Hyperf\Context\Context;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\ExceptionHandler\Formatter\FormatterInterface;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Logger\LoggerFactory;
use Mine\Support\Logger\UuidRequestIdProcessor;
use Mine\Support\Traits\Debugging;
use Psr\Container\ContainerInterface;
use Swow\Psr7\Message\ResponsePlusInterface;

abstract class AbstractHandler extends ExceptionHandler
{
    use Debugging;

    public function __construct(
        /** @phpstan-ignore-next-line */
        private readonly ConfigInterface $config,
        private readonly ContainerInterface $container,
        private readonly LoggerFactory $loggerFactory
    ) {}

    abstract public function handleResponse(\Throwable $throwable): Result;

    public function handle(\Throwable $throwable, ResponsePlusInterface $response)
    {
        $this->report($throwable);
        return value(function (ResponsePlusInterface $responsePlus)use ($throwable) {
            // 如果是 debug 模式,自動處理跨域
            if ($this->isDebug()) {
                $responsePlus
                    ->setHeader('Access-Control-Allow-Origin', '*')
                    ->setHeader('Access-Control-Allow-Credentials', 'true')
                    ->setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
                    ->setHeader('Access-Control-Allow-Headers', 'DNT,Keep-Alive,User-Agent,Cache-Control,Content-Type,Authorization');
                Context::set(self::class . '.throwable', [
                    'message' => $throwable->getMessage(),
                    'file' => $throwable->getFile(),
                    'line' => $throwable->getLine(),
                    'trace' => $throwable->getTrace(),
                ]);
            }
            return $responsePlus;
        }, $this->handlerRequestId(
            $this->handlerResult(
                $response,
                $this->handleResponse($throwable)
            )
        ));
    }

    /**
     * 上報日誌+列印錯誤.
     */
    public function report(\Throwable $throwable)
    {
        // 如果是debug模式,列印錯誤到控制檯
        if ($this->isDebug()) {
            $this->container->get(StdoutLoggerInterface::class)->error(
                $this->container->get(FormatterInterface::class)->format($throwable)
            );
        }
        $this->loggerFactory
            ->get('error')
            ->error($throwable->getMessage(), ['exception' => $throwable]);
    }

    /**
     * 處理result 打包到 response body 中.
     */
    protected function handlerResult(ResponsePlusInterface $responsePlus, Result $result): ResponsePlusInterface
    {
        $responsePlus->setHeader('Content-Type', 'application/json; charset=utf-8');

        if ($this->isDebug()){
            $result = $result->toArray();
            $result['throwable'] = Context::get(self::class . '.throwable');
            return $responsePlus
                ->setBody(new SwooleStream(Json::encode($result)));
        }

        return $responsePlus
            ->setBody(new SwooleStream(Json::encode($result)));
    }

    /**
     * 處理 response 加上 request-id 資訊.
     */
    private function handlerRequestId(ResponsePlusInterface $responsePlus): ResponsePlusInterface
    {
        return $responsePlus->setHeader('Request-Id', UuidRequestIdProcessor::getUuid());
    }
}

也就意味著,當應用發生錯誤時。如果 APP_DEBUG=true 則會列印呼叫棧以及在命令列輸出錯誤日誌 當然我們並不推薦你直接進行 throw new Exception 的操作。而是推薦使用 throw new BusinessException 丟擲業務異常 以下是預設自帶的業務異常

php
<?php

declare(strict_types=1);
/**
 * This file is part of MineAdmin.
 *
 * @link     https://www.mineadmin.com
 * @document https://doc.mineadmin.com
 * @contact  root@imoi.cn
 * @license  https://github.com/mineadmin/MineAdmin/blob/master/LICENSE
 */

namespace App\Exception;

use App\Http\Common\Result;
use App\Http\Common\ResultCode;

class BusinessException extends \RuntimeException
{
    private Result $response;

    public function __construct(ResultCode $code = ResultCode::FAIL, ?string $message = null, mixed $data = [])
    {
        $this->response = new Result($code, $message, $data);
    }

    public function getResponse(): Result
    {
        return $this->response;
    }
}
php
class TestService{

    public function test(){
        if(false){
            throw new BusinessException(ResultCode::Fail,trans('xxx'));
        }
    }
}
php

class TestController {
    
   private TestService $testService;
   // ... other code
   
   public function test(){
      $this->testService->test();
      return $this->success();
   }

}

註冊一個自定義異常

儘管預設自帶的異常處理已經足夠日常開發了,但如果遇到特殊的業務場景需要二次開發自己的異常處理器。那麼就需要 保證你的異常處理器繼承於 App\Exception\Handler\AbstractHandler. 並且實現 handleResponse 以及 isValid 方法

isValid 將當前的異常例項傳入進來。需要返回 true 或 false 來判斷當前異常是否需要捕獲,handleResponse 則是當捕獲到異常時,則呼叫此方法。並把返回的 Result 例項返回到 client

例如預設提供的 UnauthorizedExceptionHandler 異常處理類。他只處理 UnauthorizedException。也就是 未授權時才會進入到處理狀態。並且返回 403 業務碼.

致力于为品牌和企业创造价值