异常指的是在程序运行过程中发生的异常事件,通常是由外部问题所导致的。
异常处理是程序开发中经常遇到的任务,如何处理异常,从一定程度上反映了我们的程序是否足够严谨。
在本文的例子中,我将异常大致分为 用户异常 和 系统异常。
先来个未进行异常处理的例子,此处是关于商城中商品上下架展示的例子。
访问商品详情页的时候,在处理非正常流程时使用了throw new Exception
抛出异常来终止流程。
if (!$product->sale_state) {
throw new \Exception('商品未上架');
}
然后来访问一个被下架的商品测试一下
开发环境下,debug模式打开:
线上环境下,debug模式关闭:
你肯定不想用户看到这样的一个界面,这个提示很不友好。下面我们看下如何处理我上面说的用户异常和系统异常。
比如访问一个被下架的商品时触发的异常,对于此类异常我们需要把触发异常的原因告知用户。
把这类异常命名为 InvalidRequestException,可以通过 make:exception 命令来创建:
php artisan make:exception InvalidRequestException
新创建的异常文件保存在 app/Exceptions/
目录下:
app/Exceptions/InvalidRequestException.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()) {
return response()->json(['msg' => $this->message], $this->code);
}
return view('pages.error', ['msg' => $this->message]);
}
}
Laravel 5.5 之后支持在异常类中定义 render()
方法,该异常被触发时系统会调用 render() 方法来输出,我们在 render() 里判断如果是 AJAX 请求则返回 JSON 格式的数据,否则就返回一个错误页面。
错误页面:
resources/views/pages/error.blade.php
@extends('layouts.app')
@section('title', '错误')
@section('content')
<div class="card">
<div class="card-header">错误</div>
<div class="card-body text-center">
<h1>{{ $msg }}</h1>
<a class="btn btn-primary" href="{{ route('root') }}">返回首页</a>
</div>
</div>
@endsection
当异常触发时 Laravel 默认会把异常的信息和调用栈打印到日志里(storage/logs/xxx.log),但是你会发现其中有大量的日志打印到日志文件中,会干扰我们去分析真正有问题的异常,所以应该屏蔽这个行为。
Laravel 内置了屏蔽指定异常写日志的解决方案:
app/Exceptions/Handler.php
namespace App\Exceptions;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
InvalidRequestException::class,
CouponCodeUnavailableException::class,
];
...
...
当一个异常被触发时,Laravel 会去检查这个异常的类型是否在 $dontReport 属性中定义了,如果有则不会打印到日志文件中。
比如连接数据库失败,对于此类异常我们需要有限度地告知用户发生了什么,但又不能把所有信息都暴露给用户(比如连接数据库失败的信息里会包含数据库地址和账号密码),因此我们需要传入两条信息,一条是给用户看的,另一条是打印到日志中给开发人员看的。
新建一个 InternalException 类:
php artisan make:exception InternalException
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 失败对用户来说都是一样的,就是系统不可用,用户也不可能根据这个信息来解决什么问题。
接着本文中的那个未处理示例来看,通过我们上面写的异常处理类,我们现在把他运用到相关代码处,比如上面的这个商品详情展示:
...
...
public function show(Product $product, Request $request)
{
// 判断商品是否已经上架,如果没有上架则抛出异常。
if (!$product->sale_state) {
throw new InvalidRequestException('商品未上架');
}
...
...
}