用 Laravel 拥抱异常

令人讨厌的异常

提起异常,大家都很反感,当信心满满的写完一段代码,刷新页面发现上面写着大大的 Exception 是最心烦的时候了。模块给领导演示的时候,如果报了异常,也是最让人崩溃的时候了。

既然这样,我为什么要说拥抱异常呢?

其实异常并没有大家想象的那么可怕,异常其实非常简单,如果你的程序里面很少有抛出异常的代码,我相信你看了下面的分析,你会爱上异常的。

什么是异常

异常是运行中超出了你程序预期的一个东西。

异常就是一个意外,影响了你的程序正常运行。但是如果你用好异常,会让你的程序便于解耦,结构更加清晰明了。

如何使用异常

在 Laravel 中已经定义了很多异常,例如 ModelNotFoundException AuthorizationException MassAssignmentException HttpResponseException 等等,基本每个模块都会定义一些异常。

场景

例如京东有个 轻松购 的功能,当点击的时候会将该商品自动添加到购物车并生成订单,然后进行支付,这是一个网络请求,但是在后端实际执行了一系列的事情(以下操作是简单举例子便于说明问题,和真实步骤有差异)

  1. 验证用户是否登录
  2. 验证用户状态(如果被拉入系统黑名单就不能登录)
  3. 查看订单中物品是否实时有货
  4. 锁定货物(库存减少,支付中的货物数量+1)
  5. 生成订单

问题

步骤很多,如果任何一个环节出现问题,就要做响应的处理

  1. 用户没有登录就要保存购买信息,并跳转到登录页面
  2. 用户状态有问题则直接提示禁止继续购买
  3. 如果没有货物则跳转商品页面
  4. 同时购买人太多,自己购买时无货

处理思路

这个时候该如何实现这个流程?

  1. 写到一个 controller 里面,顺序执行,哪一步出错直接 return ? 这个 controller 该有多长,代码完全不可读,这是典型面向过程了。
  2. 封装几个业务方法返回 true false 判断?比第一个好,但是就像编辑器多了折叠功能,其实还是面向过程的思路。

是时候定义一个处理这个购买流程的类和一些异常了。下面是每个步骤的分析

  1. 需要在中间件验证用户是否登录,直接跳转。
  2. 可以写个中间件,命名为 BlacklistMiddleware 专门处理黑名单,也是直接跳转到禁止界面。
  3. 此时其实已经到我们的业务处理类里面了,如果无货,你还会写跳转到无货页面吗?显然这里不合适了,因为你不知道什么时候需求变更(可以继续购买,只不过等待到货而已),如果真的跟需求变更来回改核心代码,累死也写不完程序了。建一个 NoGoodsException 异常,当你业务处理类发现没有货,直接抛出该异常。然后在控制器中 try catch 捕获该异常进行后续处理,或者使用 App\Exceptions\Handler 进行统一处理。
  4. 如果你定义了上面的异常,那么你就尽情的抛出异常吧,已经有程序帮你处理后面的事情了。

这样的好处就是,你的逻辑完全分离,不要再在业务逻辑代码里面考虑如何返回什么页面,要跳转到哪里,只考虑抛出合适的异常即可,简单的可以直接在 App\Exceptions\Handler 定义通用的捕获异常处理方式,这样的表现就非常统一了。如果需求高了,可以 try catch 后再根据情况再抛更详细的异常。

实践

直接上代码,简单例子

// namespace App\Http\Controllers;
public function update($id)
{
    // 过去你可能这么写
    /*
    $post = Post::find($id);
    if ( is_null($post) ) {
        return view('errors.404')->withMessage('没有找到');
    }
    */
    // 现在直接这样写
    $post = Post::findOrFail($id);

    if (Gate::denies('view', $post)) {
        // 过去你可能会这样写
        // return view('errors.401');
        // 现在可以这样写
        throw new AuthorizationException('您不是该文章的作者,不能修改');
    }

    # ...
}
// App\Exceptions\Handler.php 
public function render($request, Exception $e)
{
    // 没有权限访问
    if ($e instanceof ForbiddenException) {
        $message      = $e->getMessage() ?: '您没有权限操作';
        $code     = $e->getCode() ?: 401;
        $redirect = $e->getRedirect() ?: route('error.401');

        return $request->ajax() || $request->wantsJson() ? response()->json([ 'message' => $message ],
            $code) : response(view('errors.401', compact('code', 'message', 'redirect')), $code);
    }

    // FirstOrFail 和 FindOrFail 异常处理
    if ($e instanceof ModelNotFoundException) {
        // 如果删除的内容已经不存在了,就没必要报错了,直接成功处理
        if ('DELETE' === strtoupper(Request::method())) {
            return Response::json([ 'success' => true ]);
        }
        if ($request->ajax() || $request->wantsJson()) {
            return response()->json([ 'message' => '没有找到' ], 404);
        } else {
            return response()->view('errors.404', [ ], 404);
        }
    }

    return parent::render($request, $e);
}

记录异常

对于某些异常,我们可能需要记录下来,以便方便发现问题,在 App\Exceptions\Handler 我们可以不去记录一些异常

    protected $dontReport = [
        ModelNotFoundException::class,
    ];

安装 Laravel 5 log viewer ,便于查看异常。

或者使用 Bugsnag 进行异常记录和分析。可参见 @Summer 的 教程:使用 Bugsnag 来监控 Laravel 应用运行健康状态

总结

异常对我们控制程序的流程来说非常重要。解耦了程序出现意想不到结果时信息传递的逻辑。每个业务模块发生异常最终通过 Laravel 的方便的异常处理,和友好的展示,并能根据情况来记录错误,这样让我们的程序更加健壮,方便开发和维护。

本帖已被设为精华帖!
本帖由 Summer 于 7年前 加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 25
monkey

学习了 :+1:

刚好之前开发的一个项目用过此种方式处理异常,个人觉得非常好用。

这样做的好处在于 Controller 里的代码能更加结构化,增加可读性,还能方便统一维护错误输出。

7年前 评论
Summer

错误异常合理运用,还是蛮方便的。

+1: 深入简出的好文

7年前 评论

嗯嗯,抛出异常来分离逻辑,我现在也是这样用,逻辑处理干净多了

7年前 评论
Summer

render 方法里还差一句:

        return parent::render($request, $e);
7年前 评论

@Summer 已添加,谢谢提醒 :heart:

7年前 评论
MRWang

不过异常也要合理的控制,比较任何情况下都可以使用异常,写太多异常看起来像 JAVA

7年前 评论
Destiny

:+1:

7年前 评论

使用了Dingo来处理,异常就不经过Exceptions/Handler了...

7年前 评论

@zjien Dingo 有自己的异常处理方式。所以需要单独处理,可以在 provider 的 boot 里面注册对异常的处理方式,例子如下

$this->app['Dingo\Api\Exception\Handler']->register(function (ModelNotFoundException $exception) {
    return Response::make([
        'message'     => '该信息不存在。',
        'status_code' => 404
    ], 404);
});
7年前 评论

'你还会写跳转到无货也没吗' ,应该是页面

7年前 评论

感觉不错 值得学习

5年前 评论

@zhuzhichao 5.5好像可以直接在异常类中写render 方法 不用再去Handler.php 中的render 方法里面接管异常了

file

5年前 评论

@不负韶华。
handler是全局统一异常,可以理解为异常的最后一道关卡。
你这儿写的是请求验证失败需要抛出的错误数据,出了request验证这块,你的异常就捕获不到了。

5年前 评论

@zhuzhichao
实际情况远比规划复杂,我就针对之前踩过的坑说个情况。
count(null)在7版本是可用的,7.2就会报错了。之前有一次就是管理服务器的同事,给php升级了导致程序崩溃。
这个考虑不到吧? :joy:
@不负韶华。
嗯,这个是我没看清楚。我误看这个是框架封装的,认为你做的请求验证的错误处理

5年前 评论

我觉得程序是没有异常的,‘异常’完全就是程序运行的预期结果,你这么写,它就这么运行,结果就该是这样,虽然结果不是程序员所预期的,那也只是因为你让程序走向你认为的‘异常’。 :joy:

5年前 评论

@Juner 这样理解也没错。是你程序顺利运行预期之外的“预期”结果。 :sweat_smile:

5年前 评论

这种方式第一次见,是在一个TP5开发小程序的教程里看到的,当时就觉得非常好,现在TP5里已经可以信手捻来了,但是Laravel还没试图搞过,可以试试~~~~

4年前 评论

那会不会造成 异常类很多呢?一个项目下来 只要感觉出错的地方 都写异常类?还有想问下 try catch 你为什么不在控制器就直接try catch 然后 控制器调用的方法就只管丢就可以了 而不需要每个方法都try catch

3年前 评论

非常感谢,但laravel 8 异常处理类handler 的render方法参数2变成Throwable,此时该咋办,百度了很久了,实在没有答案。

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!