Error & Exception (错误&异常)
php 错误和异常的知识点比我想象的要复杂,话不多说,直接上干货。
抛开php,如果让我们去设计一个错误异常处理模块,我们需要关注哪些点?
我想应该有这几方面:
- 错误类型有哪些 error_types
- 出现了错误之后是否需要报告 error_report
- 程序报告了错误之后是否需要输出错误 error_display
- 程序报告了错误之后是否需要记录错误 error_log
- 需要让应用程序自定义错误处理函数,set_error_handler
- 需要让应用程序能够获取错误的信息 error_get_last
以上就是错误处理的基本知识点,这样梳理,知识点就清楚多了,搞清楚了之后上面的问题后,甚至我们自己都可以设计一个错误处理模块
异常怎么处理呢,和错误一样,我们需要考虑:
- 异常的类型有哪些 Exception types
- 自定义捕获异常 try catch
- 如果没有自定义捕获异常的逻辑,那么没有捕获的异常怎么处理 set_exception_handler
PHP错误异常处理
error_types
(错误类型)很重要,我们需要搞清楚错误类型有哪些,为什么会触发这些错误类型
E_ERROR
致命的运行错误,这类错误一般是不可恢复的情况,例如内存的分配问题,后果是导致脚本不在继续执行,脚本不在继续执行所以set_error_handlder
不会捕获E_ERROR
, 在脚本中定义的init_set
函数也不会生效
Fatal error: require(): Failed opening required 'test.php'
E_WARNING
运行的警告,比如include 一个不存在的文件,脚本会继续执行
Warning: include(test.php): failed to open stream:
E_PARSE
编译时语法解析错误 比如少了一个;
Parse error: syntax error
php内核错误
E_CORE_ERROR
在PHP初始化启动过程中发生的致命错误。该错误类似 E_ERROR,但是是由PHP引擎核心产生的
E_COMPILE_ERROR
致命编译时错误。类似E_ERROR, 但是是由Zend脚本引擎产生的。
E_COMPILE_WARNING
编译时警告 (非致命错误)。类似 E_WARNING,但是是由Zend脚本引擎产生的
E_CORE_WARNING
PHP初始化启动过程中发生的警告 (非致命错误) 。类似 E_WARNING,但是是由PHP引擎核心产生的
这里需要搞清楚php引擎和zend脚本引擎到底是什么
简单来说,php引擎是启动php,初始化php的模块,读取和解析php.ini文件,激活zend引擎。
zend引擎是对脚本文件进行词法分析,语法分析。然后编译成opcode执行,如果安装了apc之类的opcode缓存,那么编译环节可能会被跳过而直接执行opcode。
opcode就是zend引擎定义的指令。
所以由php引擎,或者zend引擎触发的错误类型一般都是致命的错误,会导致脚本终止执行。所以set_error_handler无法捕获的这类错误。
php错误相关配置 php.ini
error_report
可以配置需要报告的错误类型, 一般在程序运行的时候需要报告所有的错误。
// 关闭所有PHP错误报告
error_reporting(0);
// 报告所有 PHP 错误 (参见 changelog)
error_reporting(E_ALL);
// 报告所有 PHP 错误, 和E_ALL不一样的地方在于-1可以报告未来可能出现的php错误
error_reporting(-1);
// 和 error_reporting(E_ALL); 一样
ini_set('error_reporting', E_ALL);
报告之后,就是需要判断是否将错误信息作为输出的一部分显示到屏幕,在调试模式下,我们当然希望把错误输出出来,
但是在线上一定不要输出错误信息
# 禁止错误输出到屏幕
ini_set('display_errors', 'off');
NOTICE 如果php脚本发生致命错误的时候,比如E_CORE_ERROR, 那么脚本会停止执行,所以在脚本中设置是无效的,只有php.ini的配置会生效
所以在线上的环境中,php.ini应该设置display_errors=off
虽然我们可能不需要把输出到屏幕,但是一定要把错误记录到日志里面,特别是线上环境,于是需要开启php错误日志
log_errors: true,
# 注意这个文件的可写权限
error_log: /tmp/php_error.log
自定义错误处理函数
php可以让用户自定义错误处理函数,重要的是要记住 error_types 里指定的错误类型都会绕过 PHP 标准错误处理程序, 除非回调函数返回了 FALSE。 error_reporting() 设置将不会起到作用而你的错误处理函数继续会被调用。
以下级别的错误不能由用户定义的函数来处理: E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、 E_COMPILE_WARNING
mixed set_error_handler ( callable $error_handler [, int $error_types = E_ALL | E_STRICT ] )
class HandlerErrors
{
public function registerErrorHandler()
{
set_error_handler(array($this, "handleError"));
}
public function handleError($level, $message, $file = '', $line = 0, $context = [])
{
# error_reporting 获取配置报告的错误类型
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
}
}
error_get_last
获取最后发生的错误, 甚至可以捕获Fatal error
The error_get_last() function will give you the most recent error even when that error is a Fatal error
这个函数一般结合register_shutdown_function
来使用
protected function registerShutdownHandler()
{
register_shutdown_function(array($this, 'handleShutdown'));
}
public function handleShutdown()
{
if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
throw new ErrorException(
$error['message'], $error['type'], 0, $error['file'], $error['line']
);
}
}
到这里,错误处理已经讲完,下面是异常处理
异常类型
所有的异常类型都继承了Exception
, 在PHP7后,Exception
都继承了Throwable
, 不同于传统(PHP 5)的错误报告机制,现在大多数错误被作为 Error 异常抛出, 如果尚未注册异常处理函数,则按照传统方式处理:被报告为一个致命错误(Fatal Error)
Error 类并非继承自 Exception 类
# 参数的类型不要设置为Exception,因为如果是抛出了ErrorException
# 并没有继承 Exception
public function handleException(Exception $e)
{
// TODO
}
# 正确做法
public function handleException(Throwable $e)
{
// TODO
}
在SPL异常,PHP为我们定义了如下几种异常
BadFunctionCallException
BadMethodCallException
DomainException
InvalidArgumentException
LengthException
LogicException
OutOfBoundsException
OutOfRangeException
OverflowException
RangeException
RuntimeException
UnderflowException
UnexpectedValueException
所以,我们在写业务代码的时候,不要习惯抛出Exception
, 我们可以不同的场景抛出不同的异常。
捕获异常
如果在业务代码中使用try_catch, 则异常会被捕获,如果没有设置try_catch, 我们可以定义全局函数set_exception_handler 捕获异常
常规try_catch做法
try {
# 业务代码
} catch (Exception $e) {
# 异常处理
} catch (ErrorException $e) {
# php7 错误处理
} finnaly {
# 最终都会执行到这里
}
自定义异常处理函数
class ErrorHandler
{
protected function registerExceptionHandler()
{
set_exception_handler(array($this, "handleException"));
}
public function handleException(throwable $e)
{
# 获取处理throwable的handler
$handler = $this->getExceptionHandler();
$handler->report($e);
if ($this->app->debugging()) {
$this->renderForConsole($e);
}
}
# laravel中需要bind的异常处理对象
protected function getExceptionHandler()
{
return $this->app->make(ExceptionHandler::class);
}
}
到这里,我们基本上了解了php是怎么处理错误和异常的,那么laravel是如何组织这些特性的呢
laravel的异常处理
laravel处理的流程是这样的:
1 配置error_report, 设置set_error_handler, set_excepiton_handler 函数
2 设置set_shutdown_function回调函数,函数执行error_get_last()获取错误
3 error_handler把错误拿到后再把错误以异常的方式throw处理统一交给excepiton_handler处理
4 在exception_handler中调用在容器中bind的exception handler去处理异常
所以在laravel 可以自己实现一个Exception handler对象
# 在handle exception 中会调用getExceptionHandler 处理异常
protected function getExceptionHandler()
{
return $this->app->make(ExceptionHandler::class);
}
Exception Handler 对象设计
当Exception Handler拿到一个对象后,会怎么处理了,在laravel Contracts中定义了几个方法
shouldntReport: 是否需要记录这个错误
report: 记录错误到日志中
render: 输出异常,把异常输出到浏览器中
renderForConsole: 输出异常,把异常输出到屏幕
public function report(Exception $e)
{
if ($this->shouldntReport($e)) {
return;
}
try {
$logger = app('log');
} catch (Exception $ex) {
throw $e;
}
$logger->error($e);
}
public function renderForConsole($output, Exception $e)
{
(new ConsoleApplication)->renderException($e, $output);
}
业务异常处理
在业务开发中,我们会抛出一些业务异常,例如请求参数不合法,或者业务代码有问题(比如RPC请求超时)
这里可特别建议,严格遵守HTTP CODE 的规范来定义Exception Code
如果是请求参数不合法,抛出Exception Code 为4xx
如果是业务代码有问题,抛出Exception Code 为5xx
# 请求餐厅的参数不合法,所以抛出400
$restaurant = Restaurant::get($id);
if (! $restaurant) {
throw new AppException(400, 'NO_VALID_RESTAURANT');
}
# 业务代码有问题,抛出了500
try {
$response = $this->sendHttpRequest($url, $params);
} catch (\Exception $e) {
throw new AppException(500, 'SMS_SEND_FAILED');
}
抛出异常和异常CODE的另外一个好处,就是可以使用异常的CODE重写HTTP CODE, 这样客户端可以根据HTTP CODE 的返回值做相应的逻辑判断
具体的做法是,catch住异常并获取到CODE,然后把CODE设置给Response对象,由response对象把code返回给client
# statusCode 就是http code
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
当一次请求完成后,需要记录此次访问日志,使用CODE另外一个好处就是根据CODE 判断日志的级别
App::finish(function ($request, $response) {
echo "excute finish\n";
$statusCode = $response->getStatusCode();
$level = 'info';
if ($statusCode >= 500) {
$level = 'error';
} elseif ($statusCode >= 400) {
$level = 'warning';
}
$route = Route::current();
$actionName = $route->getActionName();
$actionTime = intval(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']);
$message =
$actionName
.' '.json_encode($request->getParameters(), true)
.' '.$actionTime
.' '.$response->getStatusCode();
Log::$level($message)
}
现在,对PHP的错误处理应该有一个系统的了解了,最重要的是学习php异常处理的思路.
试想一下,如果让我们设计一个处理异常的模块,我们该怎么设计?