本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net

优雅地处理异常

上面我们实现邮箱验证时,处理一些非正常流程时使用了 throw new Exception 抛出异常终止流程,比如:

  1. if ($user->email_verified) {
  2. throw new Exception('你已经验证过邮箱了');
  3. }

但是这种异常提示,不够友好。

异常

在本次的项目开发中,我们将异常大致分为 用户异常系统异常

1. 用户错误行为触发的异常

比如上次已经验证过邮箱的用户再次申请激活邮件触发的异常,对于此类我们需要把触发原因告知用户。

我们把这类异常命名为 InvalidRequestException,可以通过 make:exception 命令来创建:

$ php artisan make:exception InvalidRequestException

新创建的异常文件保存在 app/Exceptions/ 目录下:

app/Exceptions/InvalidRequestException.php

<?php

  namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;

class InvalidRequestException extends Exception
{
  public function __construct(string $message = "", int $code = 400)
  {
    parent::__construct($message, $code);
  }

  public function render(Request $request)
  {
    if ($request->expectsJson()) {
      // json() 方法第二个参数就是 Http 返回码
      return response()->json(['msg' => $this->message], $this->code);
    }

    return view('pages.error', ['msg' => $this->message]);
  }
}

Laravel 5.5 后支持在异常类定义 render() 方法,当异常被触发时系统会调用 render() 方法输出,我们在 render() 里判断如果是 AJAX 请求则返回 JSON 格式数据,否则返回错误页面。

现在来创建这个错误页面:

$ touch resources/views/pages/error.blade.php

resources/views/pages/error.blade.php

@extends('layouts.app')
@section('title', '错误')

@section('content')
<div class="panel panel-default">
  <div class="panel-heading">错误</div>
  <div class="panel-body text-center">
    <h1>{{ $msg }}</h1>
    <a class="btn btn-primary" href="{{ route('root') }}">返回首页</a>
  </div>
</div>
@endsection

当异常触发时 Laravel 默认会把异常的信息和调用栈打印到日志里

而此类异常并不是因为我们系统本身的问题导致的,不会影响我们系统的运行,如果大量此类日志打印到日志文件里反而会影响我们去分析真正有问题的异常,因此需要屏蔽这个行为。

Laravel 内置了屏蔽指定异常写日志的解决方案:

app/Exceptions/Handler.php

.
.
.
    protected $dontReport = [
        InvalidRequestException::class,
    ];
.
.
.

当一个异常被触发时,Laravel 会去检查这个异常的类型是否在 $dontReport 属性中定义了,如果有则不会打印到日志文件中。

2. 系统内部异常

比如数据库连接失败,此类异常的错误信息不能全部返回,比如数据库连接失败的信息里有数据库地址和账号密码。因此我们需要输入两条信息,一条用户看,一条打印到日志,开发人员看。

新建一个 InternalException 类:

$ php artisan make:exception InternalException

app/Exceptions/InternalException.php

<?php

  namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;

class InternalException extends Exception
{
  protected $msgForUser;

  public function __construct(string $message, string $msgForUser = '系统内部错误', int $code = 500)
  {
    parent::__construct($message, $code);
    $this->msgForUser = $msgForUser;
  }

  public function render(Request $request)
  {
    if ($request->expectsJson()) {
      return response()->json(['msg' => $this->msgForUser], $this->code);
    }

    return view('pages.error', ['msg' => $this->msgForUser]);
  }
}

这个异常的构造函数第一个参数就是原本应该有的异常信息比如连接数据库失败,第二个参数是展示给用户的信息,通常来说只需要告诉用户 系统内部错误 即可,因为不管是连接 Mysql 失败还是连接 Redis 失败对用户来说都是一样的,就是系统不可用,用户也不可能根据这个信息来解决什么问题。

应用到验证邮箱功能

接下来我们要把之前验证邮箱功能中的异常替换成我们刚刚定义的异常。

app/Http/Controllers/EmailVerificationController.php

use App\Exceptions\InvalidRequestException;
.
  .
  .
  public function verify(Request $request)
{

  $email = $request->input('email');
  $token = $request->input('token');
  if (!$email || !$token) {
    throw new InvalidRequestException('验证链接不正确');
  }
  if ($token != Cache::get('email_verification_'.$email)) {
    throw new InvalidRequestException('验证链接不正确或已过期');
  }
  if (!$user = User::where('email', $email)->first()) {
    throw new InvalidRequestException('用户不存在');
  }
  .
    .
    .
  }
public function send(Request $request)
{
  $user = $request->user();
  if ($user->email_verified) {
    throw new InvalidRequestException('你已经验证过邮箱了');
  }
  .
    .
    .
    }

接下来我们看看效果:

用我们之前验证过邮箱的账号登录,然后访问 http://shop.test/email_verify_notice

点击 重新发送验证邮件 按钮,即可看到 你已经验证过邮箱了 提示。