laravel框架程序开发过程中能够优雅地处理异常,这是因为laravel
框架拥有比较好的异常处理机制。用过laravel开发电商网站的,商品详情页,在处理非正常流程时使用了throw new Exception
抛出异常来终止流程,
1.throw new Exception
抛出异常代码如下:
if (!$product->on_sale) {
throw new \Exception('商品未上架');
}
$product为商品返回数据素组,on_sale为判断商品是否上架字段,如果为真则上架,反之未上架。
尝试访问一款已经下架的产品,在开发环境下看到如下异常抛出提示:
而用户触发这个异常之后程序会跳转到500错误页面,这种异常抛出对于用户是很不友好的,接下来介绍一下Laravel项目中如何正确的优雅处理异常抛出问题。
2.异常
异常通常是由外部问题所导致的,同时异常处理是程序开发中经常遇到的任务,如何优雅地处理异常,从一定程度上反映了你的程序是否足够严谨。一般异常分为用户异常 和 系统异常,接下来我们将分别对其讲解和代码实现。
2.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="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
当异常错误被触发后,错误信息会被打印到日志中,不同的系统和环境配置可能日志存放的位置不尽相同,我这里用的是AMH面板集成的PHP环境log日志保存在如下位置:
/home/wwwroot/网站保存文件夹名称/log/access.log
比如访问的是未上架的商品,返回的此类异常,并不会影响系统的正常运行,如果大量的此类异常写入log日志文件中反而会影响我们去分析真正的异常问题,所以需要屏蔽此类异常行为。
Laravel 内置了屏蔽指定异常写日志的解决方案:
屏蔽指定异常文件所在目录
app/Exceptions/Handler.php
protected $dontReport = [
InvalidRequestException::class,
];
当一个异常被触发时,Laravel 会去检查这个异常的类型是否在 $dontReport
属性中定义了,如果有则不会打印到日志文件中。
2.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/ProductsController.php
use App\Exceptions\InvalidRequestException;
public function show(Product $product, Request $request)
{
if (!$product->on_sale) {
throw new InvalidRequestException('商品未上架');
}
}
这样我们的异常处理就完成了,这样不仅能自定义异常提示页面,还能让用户有更好的浏览体验。