程序会出错,这是不争的事实,不管我们多么努力,在项目中倾注多少时间,总会有忽略的缺陷和错误存在。
例如,你用过的PHP应用是不是经常显示空白页面?你访问PHP开发的网站时是不是见过难以理解的堆栈跟踪?出现这些不幸的情况,说明应用有错误或未被捕获的异常。
错误和异常是强大的工具,能帮助我们预期意料之外的事,使用优雅的方式捕获问题和不足。不过,错误和异常之间很相似,容易让人混淆。
错误和异常都表明出问题了,都会提供错误消息,然而,错误出现的时间比异常早。错误会导致程序脚本停止执行,如果可能,错误会委托全局错误处理程序处理,有些错误是无法恢复的,如今我们基本上只需处理异常,不用管错误,但我们仍然必须做好防御准备。
异常
异常是Exception类的对象,在遇到无法修复的情况时抛出(例如,远程API无响应,数据库查询失败,或者无法满足前置条件)。出现这些问题时,异常用于主动出击,委托职责,异常还可以用于防守,预测潜在的问题,减轻其影响。
Exception是所有用户级异常的基类。
参考链接:https://www.php.net/manual/zh/class.exception.php
<?php class Exception implements Throwable { protected $message = 'Unknown exception'; // 异常信息 private $string; // __toString 的缓存 protected $code = 0; // 用户自定义异常错误码 protected $file; // 发生异常的源文件名 protected $line; // 发生异常的源代码行号 private $trace; // backtrace private $previous; // 如果是嵌套异常,则是之前的 exception public function __construct($message = '', $code = 0, Throwable $previous = null); final private function __clone(); // 禁止克隆异常。 final public function getMessage(); // 异常信息 final public function getCode(); // 异常错误码 final public function getFile(); // 发生异常的源文件名 final public function getLine(); // 发生异常的源代码行号 final public function getTrace(); // backtrace() 数组 final public function getPrevious(); // 之前的 exception final public function getTraceAsString(); // 已格成化成字符串的 getTrace() 信息 // Overrideable public function __toString(); // 可输出的格式化后的字符串 }
如果使用自定义的类来扩展内置异常处理类,并且要重新定义构造函数的话,建议同时调用 parent::__construct() 来确保所有的变量已赋值。当对象要输出字符串的时候,可以重载 __toString() 并自定义输出的样式。
异常子类
PHP标准库提供可下述额外的Exception子类,扩充了PHP内置的异常类。
1:BadFunctionCallException
定义:如果回调函数引用未定义的函数或缺少一些参数,则引发异常。
2:BadMethodCallException
定义:当一个回调方法是一个未定义的方法或缺失一些参数时会抛出该异常。
3:DomainException
定义:如果值不符合定义的有效数据域,则引发异常。
4:InvalidArgumentException
定义:如果参数不是预期类型,则引发异常。
5:LengthException
定义:长度无效时抛出的异常。
6:LogicException
定义:表示程序逻辑错误的异常。 这种异常应该直接导致代码中的修复。
7:OutOfBoundsException
定义:如果值不是有效的键,则引发异常。这表示在编译时无法检测到的错误。
8:OutOfRangeException
定义:请求非法索引时引发的异常。这表示应该在编译时检测到的错误。
9:OverflowException
定义:将元素添加到完整容器时引发异常。
10:RangeException
定义:异常抛出,以指示程序执行期间的范围错误。通常这意味着除了under/overflow之外还有一个算术错误。
11:RuntimeException
定义:如果发生只能在运行时发现的错误,则会引发异常。
12:UnderflowException
定义:在空容器上执行无效操作(如删除元素)时引发的异常。
13:UnexpectedValueException
定义:如果值与一组值不匹配,则抛出异常。通常,当一个函数调用另一个函数并期望返回值是特定类型或不包括算术或缓冲区相关错误的值时,就会发生这种情况。
各个子类针对特定的情况,而且提供了上下文,说明为什么抛出异常。例如,如果PHP组件中的方法应该使用有五个字符的字符串参数,但是传入的字符串只有两个字符,那么这个方法就可以抛出InvalidArgumentException实例。
PHP的异常是类,因此可以轻易扩展Exception类,使用定制的属性和方法创建自定义的异常子类。
抛出异常
实例化时可以把异常赋值给变量,不过一定要把异常抛出。如果你编写的代码是提供给其他开发者使用的,遇到异常情况时要主动出击,也就是说,如果代码遇到了异常状况,或者在当前条件下无法操作,要抛出异常。
抛出异常后代码会立即停止执行,后续的PHP代码都不会运行。抛出异常的方式是使用throw关键字,后面跟着要抛出的Exception实例:
<?php throw new Exception("Something went wrong . Time for lunch!");
捕获异常
我们应该捕获抛出的异常,然后使用优雅的方式处理。
预测,捕获并处理异常是我们自己的责任,未捕获的异常会导致PHP应用终止运行,显示致命错误信息,而更糟的是,可能会暴露敏感的调试详细信息,让应用的用户看到。因此,我们一定要捕获异常,然后使用优雅的方式处理。
拦截并处理异常的方式是,把可能抛出异常的代码放在try/catch块中。
<?php
declare(strict_types=1);
function divide(int $num1, int $num2): int
{
if ($num2 == 0) {
throw new UnexpectedValueException('number2 cannot be zero.');
}
return $num1 / $num2;
}
try {
$num1 = 10;
$num2 = 0;
if (!(is_int($num1) && is_int($num2))) {
throw new InvalidArgumentException('params must be integer ');
}
$sum = divide($num1, $num2);
} catch (\InvalidArgumentException $e) {
echo 'message = ' . $e->getMessage() . ', code = ' . $e->getCode();
} catch (\UnexpectedValueException $e) {
echo 'message = ' . $e->getMessage() . ', code = ' . $e->getCode();
}
大家看见divide方法可能会有点疑问:这个方法要求返回参数是int,那么里面抛出了异常,那这个是不是有问题呢?
那么下面我们来解释一下:当一个方法抛出异常时,它实际上并不返回任何值。异常会中断方法的正常执行流程,跳转到调用方的catch
块。因此,在定义可能抛出异常的方法的返回类型时,我们只需要考虑正常执行时的返回类型。
还有一个就是我们常见的数据库错误,例如当查询了不存在的字段或者不存在的表,或者最终拼接的sql有语法错误,我看也可以用异常来捕获。
例如:我们现在有一个接口,要根据参数查出对应的订单详情。
$request = \Yii::$app->request; $outOderNo = $request->get('out_order_no'); $sql = 'select * from duo_order_detail as d join duo_order as o on d.out_order_no=o.out_order_no where out_order_no=:out_order_no'; $result = \Yii::$app->db->createCommand($sql)->bindParam(":out_order_no", $outOderNo)->queryOne(); return $this->asJson(['code' => 0, 'msg' => '', 'data' => $result]);
我们接着用apipost调试一下接口,发现报错了
因为前后端定义好的格式是json格式,你现在返回一个html,如果前端没有加上异常处理,数据无法解析,会导致错误,如果是app的话可能会导致闪退。
这个时候我们就要加上异常处理,不管sql有什么问题,我们返回给前端的数据都要是json数据。我们从上图可以看到,这是一个yii\db\IntegrityException异常,这是一个自定义异常子类。那么我们只要在代码中针对这种异常进行捕获并处理即可。
class IntegrityException extends Exception
{
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return 'Integrity constraint violation';
}
}
<?php $request = \Yii::$app->request; try { $sql = 'select * from duo_order_detail as d join duo_order as o on d.out_order_no=o.out_order_no where out_order_no=:out_order_no'; $result = \Yii::$app->db->createCommand($sql)->bindParam(":out_order_no", $outOderNo)->queryOne(); } catch (IntegrityException $e) { //在这个地方,可以加上对应的处理逻辑,例如邮件钉钉等方式通知技术 return $this->asJson(['code' => 1, 'msg' => 'Here is your prompt message ', 'data' => []]); } return $this->asJson(['code' => 0, 'msg' => '', 'data' => $result]);
我们从上图中看到,就算sql有错误,我们返回的格式还是json格式。
finally类
我们可以使用多个catch块拦截多种异常,如果要使用相同的方式处理抛出的不同异常类型,我们可以使用finllly类,finllly类会在捕获任何类型的异常之后运行一段代码。
<?php declare(strict_types=1); function sum(int $num1, int $num2): int { return $num1 + $num2; } try { $num1 = 1; $num2 = 2; if (!(is_int($num1) && is_int($num2))) { throw new InvalidArgumentException('params must be integer '); } $sum = sum($num1, $num2); $arr = [1, 2, 3, 4]; if (!array_key_exists(4, $arr)) { throw new OutOfRangeException ('Index does not exist'); } } catch (\InvalidArgumentException $e) { /** * 处理InvalidArgumentException异常 */ echo 'message=' . $e->getMessage() . ',code=' . $e->getCode() . PHP_EOL; } catch (\Exception $e) { /** * 处理所有异常 */ echo 'message = ' . $e->getMessage() . ', code = ' . $e->getCode() . PHP_EOL; } finally { echo 'this is finally'; } root@4fab6becb32d:/var/www/haiye# php demo.php message = Index does not exist, code = 0 this is finally
注册全局异常处理程序
那么如何捕获每个可能抛出的异常呢?答案就是PHP允许我们注册一个全局异常处理程序,捕获所有未被捕获的异常。
我们一定要设置一个全局异常处理程序。异常处理程序是最后的安全保障,如果没有成功捕获并处理异常,通过这个措施可以给PHP应用的用户显示合适的错误信息,而不是直接暴露出网站的敏感信息。
异常处理程序使用set_exception_handler()函数注册,用于没有用 try/catch 块来捕获的异常。 在 exception_handler
调用后异常会中止。
<?php declare(strict_types=1); //注册异常处理程序 function exception_handler($exception) { //处理并记录异常 echo "Uncaught exception: ", $exception->getMessage(), "\n"; } set_exception_handler('exception_handler'); //我们编写的其他代码 function sum(int $num1, int $num2): int { return $num1 + $num2; } try { $num1 = 1; $num2 = 2; if (!(is_int($num1) && is_int($num2))) { throw new InvalidArgumentException('params must be integer '); } $sum = sum($num1, $num2); $arr = [1, 2, 3, 4]; if (!array_key_exists(4, $arr)) { throw new OutOfRangeException ("Index does not exist"); } var_dump($arr[4]); } catch (\InvalidArgumentException $e) { //处理InvalidArgumentException异常 echo 'message=' . $e->getMessage() . ',code=' . $e->getCode() . PHP_EOL; } //还原成之前的异常处理程序 restore_exception_handler(); root@4fab6becb32d:/var/www/haiye/web# php demo.php Uncaught exception: Index does not exist
我们可以看到我们抛出了一个OutOfRangeException,但没有对应的catch代码,最终由全局异常处理程序来捕获了。
Error异常
PHP 7 改变了大多数错误的报告方式。不同于传统(PHP 5)的错误报告机制,现在大多数错误被作为 Error 异常抛出。
这种 Error 异常可以像 Exception 异常一样被第一个匹配的 try / catch 块所捕获。如果没有匹配的 catch 块,则调用异常处理函数(事先通过 set_exception_handler() 注册)进行处理。 如果尚未注册异常处理函数,则按照传统方式处理:被报告为一个致命错误(Fatal Error)。
Error 类并非继承自 Exception 类,所以不能用 catch (Exception $e) { … } 来捕获 Error。你可以用 catch (Error $e) { … },或者通过注册异常处理函数( set_exception_handler())来捕获 Error。
Error是所有PHP内部错误类的基类。
参考链接:https://www.php.net/manual/zh/class.error.php
<?php
class Error implements Throwable {
/* 属性 */
protected string $message = "";
private string $string = "";
protected int $code;
protected string $file = "";
protected int $line;
private array $trace = [];
private ?Throwable $previous = null;
/* 方法 */
public __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
final public getMessage(): string
final public getPrevious(): ?Throwable
final public getCode(): int
final public getFile(): string
final public getLine(): int
final public getTrace(): array
final public getTraceAsString(): string
public __toString(): string
private __clone(): void
}
Error异常子类:
定义:当执行数学运算发生错误时抛出 ArithmeticError 。 这些错误包括尝试执行负数的位移,以及对任何可能会导致值超出 int 的范围 intdiv() 调用。
2:DivisionByZeroError
定义: 当除数为零时被抛出。
3:AssertionError
定义:在函数 assert() 断言失败时被抛出。
定义:针对一些编译错误抛出的,之前是会发出致命错误。
5:ParseError
定义:当解析 PHP 代码时发生错误时抛出,比如当 eval()被调用出错时。
会抛出TypeError 的情况:
- 为类属性设置的值与该属性申明的类型不匹配。
- 传递给函数的参数类型与函数预期声明的参数类型不匹配。
- 函数返回的值与声明的函数返回类型不匹配
7:ArgumentCountError
定义:当传递给用户定义的函数或方法的参数太少时被抛出。
8:ValueError
定义:当参数类型正确但是值不正确的时候会抛出 ValueError。 例如,当函数期望是正整数时传递负整数, 或者当函数期望它不为空时传递空字符串/数组。
9:UnhandledMatchError
定义:当传递给 match 表达式的主体未被 match 表达式的任何分支处理时, 将会抛出 UnhandledMatchError。
10:FiberError
定义:当在 Fiber 上执行无效操作时,会抛出 FiberError。
PHP 7中Error 异常的引入,使得错误和异常处理更为统一。开发者可以通过捕获Error 异常,更容易地处理程序中的错误情况,提高代码的健壮性和可维护性。
捕获Error异常
<?php function sum(int $num1, int $num2): int { return $num1 + $num2; } $sum = sum(100, "test"); var_dump($sum); root@4fab6becb32d:/var/www/haiye# php demo.php PHP Fatal error: Uncaught TypeError: Argument 2 passed to sum() must be of the type int, string given, called
从上面我们可以看到有一个TypeError,那么我们针对TypeError来捕获一下
<?php function sum(int $num1, int $num2): int { return $num1 + $num2; } try { $sum = sum(100, "test"); var_dump($sum); } catch (TypeError $e) { echo "this is type error"; exit; } root@4fab6becb32d:/var/www/haiye# php demo.php this is type error
错误
参考链接:https://www.php.net/manual/zh/language.errors.basics.php
在PHP中,错误(errors)是指在程序运行过程中发生的各种问题。这些问题可能是由于语法错误、运行时错误或逻辑错误引起的。根据错误的严重程度,PHP将错误分为不同的级别。以下是一些常见的PHP错误级别:
- E_ERROR:致命的运行时错误,这类错误导致脚本终止执行。例如,调用未定义的函数或类。
- E_WARNING:运行时警告,这类错误不会导致脚本终止执行,但可能会导致预期之外的结果。例如,使用未定义的变量或包含不存在的文件。
- E_NOTICE:运行时通知,这类错误表示可能存在的问题,但不一定会导致错误。例如,使用未初始化的变量或数组的未定义索引。
- E_PARSE:解析错误,这类错误是由于PHP代码中的语法错误导致的。例如,漏掉分号或花括号不匹配。
- E_DEPRECATED:过时的功能警告,这类错误提示开发者某些功能在未来版本中可能会被废弃。
- E_STRICT:严格标准错误,这类错误是为了建议如何编写更好、更兼容的代码。
错误处理
你可以使用 error_reporting()
函数设置报告哪些错误级别。例如,如果你想报告所有错误,可以使用 error_reporting(E_ALL)
。你还可以使用 ini_set()
函数设置 display_errors
和 log_errors
选项,以控制错误信息是显示在页面上还是记录到日志文件中。
<?php // 关闭所有PHP错误报告 error_reporting(0); // Report simple running errors error_reporting(E_ERROR | E_WARNING | E_PARSE); // 报告 E_NOTICE也挺好 (报告未初始化的变量 // 或者捕获变量名的错误拼写) error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE); // 除了 E_NOTICE,报告其他所有错误 error_reporting(E_ALL ^ E_NOTICE); // 报告所有 PHP 错误 (参见 changelog) error_reporting(E_ALL); // 报告所有 PHP 错误 error_reporting(-1); // 和 error_reporting(E_ALL); 一样 ini_set('error_reporting', E_ALL);
一般情况下,报告错误的方式要遵守下述四个规则
- 一定要让PHP报告错误
- 在开发环境中要显示错误
- 在生产环境中不能显示错误
- 在开发环境和生产环境中都要记录错误
那么根据上面的四个规则。
php.ini开发环境配置:
;显示错误
display_startup_errors=On
display_errors=On
;报告所有错误
error_reporting=-1
;记录错误
log_errors=On
php.ini生产环境配置
;不显示错误
display_startup_errors=Off
display_errors=Off
;除了注意事项之外,报告所有其他错误
error_reporting=E_ALL & ~E_NOTICE
;记录错误
log_errors=On
上面这些参数的具体含义,我们可以通过https://www.php.net/manual/zh/errorfunc.configuration.php来进行进一步的了解。
这两种环境的主要区别在于:在开发环境中执行PHP脚本时要在输出中显示错误,而在生产环境则不显示。不过,这两个环境都记录了错误,如果生产环境中的PHP应用有缺陷,可以查看PHP日志文件中的详情。
注册全局错误处理程序
参考链接:https://www.php.net/manual/zh/function.set-error-handler
如果 PHP 默认错误处理器还不能满足要求,用户可以通过 set_error_handler() 设置自定义错误处理器,可处理很多类型的错误。
set_error_handler()可以用你自己定义的方式来处理运行中的错误, 例如,在应用程序中严重错误发生时,或者在特定条件下触发了一个错误(使用 trigger_error())。
默认情况下,自定义错误处理器会捕获以下错误级别:
- E_WARNING:运行时警告。
- E_NOTICE:运行时通知。
- E_USER_ERROR:自定义的致命错误。
- E_USER_WARNING:自定义的警告。
- E_USER_NOTICE:自定义的通知。
- E_STRICT:严格标准错误。
- E_RECOVERABLE_ERROR:可捕获的致命错误。
- E_DEPRECATED:过时的功能警告。
- E_USER_DEPRECATED:自定义的过时警告。
注意:E_ERROR、E_PARSE 和 E_CORE_ERROR 这几种错误级别通常不会被自定义错误处理器捕获,因为它们是致命错误,导致脚本停止执行。从 PHP 7 开始,许多导致 E_ERROR 的错误现在被转换为 Error 异常,因此可以使用异常处理机制(如 try-catch
语句)进行捕获和处理。
通过在 set_error_handler()
函数的第二个参数中指定错误级别,您可以自定义自定义错误处理器捕获的错误级别。例如,如果您只想捕获 E_WARNING 和 E_USER_WARNING 错误,可以将第二个参数设置为 E_WARNING | E_USER_WARNING
,如下所示:
set_error_handler(‘custom_error_handler’, E_WARNING | E_USER_WARNING);
下面是一个注册全局错误处理程序的代码
<?php //注册错误处理程序 function myErrorHandler($errno, $errstr, $errfile, $errline) { if (!(error_reporting() & $errno)) { return false; } $errstr = htmlspecialchars($errstr); switch ($errno) { case E_USER_ERROR: echo "<b>My ERROR</b> [$errno] $errstr<br />\n"; echo " Fatal error on line $errline in file $errfile"; echo ", PHP " . PHP_VERSION . " (" . PHP_OS . ")<br />\n"; echo "Aborting...<br />\n"; exit(1); case E_USER_WARNING: echo "<b>My WARNING</b> [$errno] $errstr<br />\n"; break; case E_USER_NOTICE: echo "<b>My NOTICE</b> [$errno] $errstr<br />\n"; break; default: echo "Unknown error type: [$errno] $errstr<br />\n"; break; } return true; } //我们编写的应用其他代码 function scale_by_log($vect, $scale): ?array { if (!is_numeric($scale) || $scale <= 0) { trigger_error("log(x) for x <= 0 is undefined, you used: scale = $scale", E_USER_ERROR); } if (!is_array($vect)) { trigger_error("Incorrect input vector, array of values expected", E_USER_WARNING); return null; } $temp = array(); foreach ($vect as $pos => $value) { if (!is_numeric($value)) { trigger_error("Value at position $pos is not a number, using 0 (zero)", E_USER_NOTICE); $value = 0; } $temp[$pos] = log($scale) * $value; } return $temp; } $old_error_handler = set_error_handler("myErrorHandler"); $a = array(2, 3, "foo", 5.5, 43.3, 21.11); $b = scale_by_log($a, M_PI); print_r($b); //还原成之前的错误处理程序 restore_error_handler(); root@4fab6becb32d:/var/www/haiye/web# php demo.php <b>My NOTICE</b> [1024] Value at position 2 is not a number, using 0 (zero)<br /> Array ( [0] => 2.2894597716988 [1] => 3.4341896575482 [2] => 0 [3] => 6.2960143721717 [4] => 49.566804057279 [5] => 24.165247890281 )
案例:配置全局异常处理程序
下面我们以yii框架为例,配置全局异常处理程序。参考链接:https://www.yiiframework.com/doc/guide/2.0/zh-cn/runtime-handling-errors。其他的PHP框架也有类似配置,大家可以去官网查一下。
这样配置的话,假如有部分异常或者错误没有捕捉到,也能直接返回默认的json数据了。
总结
那是不是所有代码都需要加上try catch呢?那其实大可不必,建议在以下几种情况加上 try-catch 语句来处理异常:
- 文件操作:当您在 PHP 中读取或写入文件时,可能会遇到一些异常情况,例如文件不存在、文件权限不足等。
- 数据库操作:当您在 PHP 中连接和操作数据库时,可能会遇到一些异常情况,例如连接失败、SQL 语句错误等。
- 远程 API 调用:当您在 PHP 中调用远程 API 时,可能会遇到一些异常情况,例如 API 返回错误、网络连接失败等。
- 外部依赖库:当您使用 PHP 调用外部依赖库时,例如发送电子邮件、调用第三方 SDK 等,都可能会出现异常情况。
- 网络请求:当您在 PHP 中发送网络请求时,例如使用 cURL、fsockopen 等函数时,都可能会出现异常情况,例如连接超时、连接中断等
异常这边的话,我们可以注册一个全局异常处理程序来统一返回格式数据。至于错误这边,建议配置好对应的php.ini,不能直接暴露代码就可以了。
注意:捕获到的异常和错误需要记录到日志文件里面,同时发送邮件给对应的相关的负责人员以及对应的钉钉群,方便及时处理问题。
参考资料:
https://www.php.net/manual/zh/spl.exceptions.php
https://www.php.net/manual/zh/class.exception
https://www.php.net/manual/zh/function.set-exception-handler.php
https://www.php.net/manual/zh/function.error-reporting.php
https://www.php.net/manual/zh/language.exceptions.extending.php
https://www.laruence.com/2012/02/02/2515.html
《Modern PHP》
《深入PHP面向对象,模式与实践》