一、介绍
文本将从PHP原生错误/异常 讲解到 Laravel 错误/异常,耐心看完,相信你会有所收获。
1.1 什么是错误
在 PHP 中,最常见错误的级别有
错误类型 | 解释 |
---|---|
Deprecated | 比如 API 过期 ,属于低级错误 |
Notice | 变量未定义 |
Warning | 结果不符合逻辑,比如函数里面 $num + 100,但 $num 传递进来的是 ‘ab’ |
Fetal | 致命错误,直接终止运行后面的运行,比如调用不存在的函数 |
Prase | 最高级别错误,以上的都是运行期间报错,这里直接在语法解析期间报错。 |
给上面对应的错误类型举几个例子
<?php
// Notice 级别:未定义变量
echo($abc);
function foo($num){
echo $num + 100;
}
// Warning 级别:传递错误的值
foo('hello');
// Fetal 级别:致命级别,导致后面的程序不在执行,比如调用不存在的函数
// throw new Error("你也可以手动通过 new error 抛出致命错误");
abc();
echo "这句话将不会被打印出来";
// Prase 级别:最高级别,以上都是运行期间错误,这里直接在语法解析期间错误
// 为了上面能正常运行,这里先注释掉
// echo ++;
另外,PHP 也提供了
set_error_handler
,此函数将会把所有把错误交给你接管,而 Laravel 正是利用了此函数进行重写,比如格式化输出、将所有错误以异常抛出去等等,具体怎么使用这里不在讲述,可参考文档。
1.2 什么是异常
PHP 是在 PHP5 后才引入异常的,每个语言对异常的理解各有不同,比如在 PHP 中 除 0 它是一个 warning 级别错误行为而不是异常,而在 JAVA 中它是属于异常行为。
$abc = 10 / 0;
// 10 / 0 属于报错 warning 行为,并不属于异常。
echo $abc;
所以很多时候异常都是由我们通过 if else 来手动控制抛出的,比如用户手机未输入
<?php
try {
if (empty($_GET['phone'])) {
throw new Exception('请输入手机号'); // 手动抛出
}
} catch (Exception $e) {
echo $e->getMessage(); // 接受异常传递过来的消息,我们直接打印。
}
注意:捕获异常需要用到 try / catch,如果直接在外部使用 throw new Exception
,它将会是一个错误行为
throw new Exception('请输入手机号');
其实异常更多的作用是用来进行一些补救措施,比如数据库事务的回滚,
举个简单例子:用户提交订单
beginTransaction(); // 启动事务
try {
DB::update('从数据库中扣掉商品库存');
// 发现用户未输入手机号,我们手动抛出异常
if (empty($_POST['phone'])) {
throw new Exceptions('请输入手机');
}
commit(); // 提交事务:由于上面抛出异常,这里不会触发。
} catch(Exception $e) {
rollback(); // 回滚事务:这里捕获到了异常,所以对扣掉库存这一操作进行回滚。
}
值得注意的是,在 PHP 7 以前,try/catch
是捕获不了错误的,比如调用不存在的函数
try {
abc();
} catch(Exception $e) {
echo '函数不存在' . $e->getMessage();
}
后来为了让部分错误
能够捕获到,在 PHP 7 中引入了 Throwable
,可以说是 Exception 的升级版,但它只能对部分错误
进行捕获,像下面的调用未知函数就可以捕获到。
try {
abc();
// 你也可以像 Exception 一样手动抛出
// throw new Throable("xxxx");
} catch (Throwable $e) {
echo "函数不存在:" . $e->getMessage();
}
而像下面的访问未定义变量,依然还是捕获不了,直接一个报错行为。
try {
echo $abc;
} catch (Throwable $e){
echo "变量不存在" . $e->getMessage();
}
另外,PHP 也提供了
set_exception_handler
,次函数将所有异常都交给你接管, 显然 Laravel 也不会放过这个函数。
好了,接下来我们开始进入 Laravel
二、Laravel 中的错误与异常
理解了 PHP 中的基本错误/异常后,现在我们来开始使用 Laravel 中的错误/异常。
本文假设你已创建 Laravel 项目。
2.1 错误与异常的默认处理
我们首先抛出一个错误,看看在 Laravel 中会发生什么,假设我们有个 test 路由。
// route/web.php
use Illuminate\Support\Facades\Route;
Route::get('test', function(){
echo $abc; // 打印不存在的变量。
});
接下来访问 http://localhost:8000/test ,输出如下:
可以看到,Laravel 展示的是一个美化过的错误页面。
另外我们也知道原生 try/catch
是无法捕获到未知变量这种错误行为的,但 Laravel 为我们做到了
use Illuminate\Support\Facades\Route;
Route::get('/test', function(){
try {
echo $abc;
} catch(Exception $e) {
echo '变量不存在:'.$e->getMessage();
}
});
很明显 Laravel 利用 set_error_handler
对错误进行了重写并以异常的形式抛出,这点值得称赞。
2.2 关于错误/异常发生后,Laravel 的日志记录问题
每次发生异常或者报错 Laravel 都会为我们写入日志文件里,下面是针对 try 内外的情况进行分析。
try 外
- 两者都被记录在
app/storage/logs
里面
echo $abc;
throw new Exception("抛出异常");
try 内
- 异常不会被记录,但可以手动调用
report()
进行记录,而错误会自动被记录。
try {
throw new Exception("抛出异常"); // 不会被记录,只能在 catch 中手动记录。
// echo $abc; // 会自动被记录
} catch (Exception $e) {
report(); // 手动记录
// .. do something
}
我们只需记得:使用 try/catch 后,权限交给用户,不使用则交给 Laravel
2.3 给 log 日志添加额外数据
每次发生异常或错误时我们想要给日志增加额外数据方便后续排查,需要怎么做?很简单,只需在 app/Exceptions/Handler.php
里面新增 context 方法进行添加即可,比如
// app/Exceptions/Handler.php =============
public function context() {
return array_merge(parent::context(), [
'user' => 'Cookcyq',
]);
}
2.4 自定义错误/异常页面
如上所知,默认情况下 Laravel 会自动返回一个美化过的页面,如果你不喜欢这种风格,Laravel 还提供了 $this->renderable
让你重写,我们来改造一下,依然是在 app/Exceptions/Handler.php
里面修改。
// app/Exceptions/Handler.php ==================
public function register(){
// 重写
$this->renderable(function(Throwable $e) {
return response([
'msg' => $e->getMessage(),
'line' => $e->getLine(),
]);
});
}
// route/web.php ==================
use Illuminate\Support\Facades\Route;
Route::get('/test', function(){
echo $abc;
// 或 throw new Exception("Something is wrong.");
});
现在我们来访问 http://localhost:8000/test
可以看到页面变成我们自定义的了,想要更多日志操作可参考官方文档:https://laravel.com/docs/9.x/errors
三、Laravel 自定义异常
由于上面的 $this->renderable
是全局性的,每个异常/错误都会触发,有时我们并不像这样做,而是仅针对某个功能,其它则按照 Laravel 默认模板正常显示,比如我们有个注册,如果注册有问题,就抛出注册异常,正好 Laravel 也为我们提供了这种能力。
- 首先在
app/Exceptions/
目录下新增RegisterException.php
文件,代码如下:
<?php
namespace App\Exceptions;
use Exception;
class RegisterException extends Exception
{
/**
* RegisterException 添加日志额外内容,可与全局 context 累加。
*/
public function context() {
return ['myException' => 'Cookcyq'];
}
/* 自定义响应内容 */
public function render($request) {
return response([
'msg' => $this->getMessage()
]);
}
}
route/web.php
代码如下
use Illuminate\Support\Facades\Route;
use App\Exceptions\RegisterException;
Route::get('/test', function(){
throw new RegisterException("Please make sure your account or password are correct.");
});
效果
四、业务案例
在实际业务中,假如我们是 API 提供者,我们需要对传递进来的参数不正确、残缺等情况进行响应处理,这个时候我们就可以利用自定义 Exception。
- 还是老样子,首先在
app/Exceptions/
新建MyException.php
文件,代码如下
<?php
namespace App\Exceptions;
use Exception;
class MyException extends Exception
{
public function render($request) {
$data = [
'msg' => $this->getMessage(),
'status' => $this->code
];
// JSON_UNESCAPED_UNICODE 解决中文变成 unicode 问题。
return response()->json($data)->setEncodingOptions(JSON_UNESCAPED_UNICODE);
}
}
- 定义
/login
路由,代码如下
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Exceptions\MyException;
Route::get('/login', function(Request $request){
if (empty($request['phone'])) {
throw new MyException("请输入手机", -10000);
}
if (empty($request['password'])) {
throw new MyException("请输入密码", -10000);
}
return response([
'msg' => '登录成功',
'status' => 10000,
]);
});
此时我们访问:http://localhost:8000/login
访问 http://localhost:8000/login?phone=10086&password=secret
怎么样,异常的处理是不是很简单?
总结
Laravel 利用了 set_error_handler / set_exception_handler
对错误/异常进行了重写,给开发者带来了许多便利之处。
希望本文能让你对 PHP 原生和 Laravel 中错误/异常的区别有一定的认识。