PHP 错误与异常处理机制
1. 错误 (Errors)
在许多“重异常 (exception-heavy)”的编程语言中,一旦发生任何错误,系统就会抛出一个异常。这无疑是一种可行的做法,但 PHP 传统上是一种**“轻异常 (exception-light)”**的编程语言。
虽然 PHP 确实支持异常,并且越来越多的核心代码在处理对象时开始使用异常,但 PHP 自身的大部分机制在发生意外时,只要不是致命错误 (Fatal Error),都会尝试继续往下执行。
例如,在 PHP 交互式 Shell 中运行:
$ php -a
php > echo $foo;
Notice: Undefined variable: foo in php shell code on line 1这仅仅抛出了一个 Notice (提示) 级别的错误,PHP 会愉快地继续执行后续代码。对于从“重异常”语言转过来的开发者来说,这可能很令人困惑。比如在 Python 中引用一个不存在的变量,程序会直接崩溃并抛出异常:
$ python
>>> print foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'foo' is not defined本质的区别在于:Python 对任何小问题都会“大惊小怪”,以此确保开发者捕获到所有潜在的边界问题;而 PHP 则倾向于“只要不致命就继续跑”,并在发生错误时仅仅记录或报告它。
1.1 错误严重级别 (Error Severity)
PHP 拥有多个错误严重级别。最常见的三种类型是:
- E_ERROR (错误): 致命的运行时错误。通常由代码中的严重缺陷引起,必须修复,因为它们会导致 PHP 脚本停止执行。
- E_WARNING (警告): 非致命错误。脚本的执行不会被中止。
- E_NOTICE (提示): 建议性信息。通常由于代码执行期间可能产生问题的行为引起,但脚本执行不会被中止。
另一种在编译时报告的错误类型是 E_STRICT。这些信息用于建议你修改代码,以确保与 PHP 未来版本的最佳互操作性和向前兼容性。
1.2 更改 PHP 的错误报告行为
你可以通过修改 PHP 配置文件或在代码中调用函数来改变错误报告的行为。使用内置函数 error_reporting(),你可以通过传递预定义的错误级别常量,来设置当前脚本执行期间生效的错误级别。
例如,如果你只想看到“错误 (Errors)”和“警告 (Warnings)”,而不想看到“提示 (Notices)”,你可以这样配置:
<?php
// 只报告致命错误和警告
error_reporting(E_ERROR | E_WARNING);你还可以控制是将错误显示在屏幕上(适合开发环境)还是隐藏并记录到日志中(适合生产环境)。更多信息,请查阅 PHP 官方的错误报告 (Error Reporting) 章节。
1.3 内联错误抑制 (Inline Error Suppression)
你可以使用错误控制运算符 @ 告诉 PHP 抑制特定的错误。将这个运算符放在表达式的开头,任何直接由该表达式产生的错误都会被静音。
<?php
// 使用 @ 抑制未定义变量或数组索引带来的 Notice 错误
echo @$foo['bar'];如果 $foo['bar'] 存在,它会正常输出;如果变量 $foo 或键 'bar' 不存在,它会返回 null 且什么也不打印,而不会抛出讨厌的 PHP Notice。
虽然这听起来像个好主意,但它带来了几个极不可取的副作用:
- 性能损耗: PHP 处理带有
@的表达式比不带@的表达式性能更差。如果性能对你的应用至关重要,请理解该运算符带来的性能影响。 - 彻底吞噬错误: 错误控制运算符会彻底吞噬错误。错误不仅不会显示在屏幕上,也不会被写入错误日志。此外,在标准的生产环境中,没有办法通过配置全局关闭错误控制运算符。你可能认为你隐藏的只是无害的错误,但其他严重得多的错误也会被同样静音,导致排错如坠云雾。
如果能避免使用错误抑制符,就坚决不要用。 例如,上面的代码完全可以使用 PHP 7 引入的“空合并运算符”优雅重写:
<?php
// 正确的做法:使用空合并运算符 (Null Coalescing Operator)
echo $foo['bar'] ?? '';唯一一个使用错误抑制符还算合理的场景可能是使用 fopen() 加载文件时。你可以在加载前检查文件是否存在,但如果文件在检查之后、fopen() 调用之前被删除了(这并非不可能发生的竞态条件),fopen() 会返回 false 并触发一个错误。这本应是 PHP 自身设计需要改进的地方,但在目前,错误抑制符似乎是解决此极端情况的唯一有效方案。
调试技巧: 前文提到标准 PHP 无法全局关闭 @ 的作用。但是,如果你安装了 Xdebug 扩展,你可以使用 xdebug.scream 配置来让所有的错误控制运算符失效。
在 php.ini 中设置:
xdebug.scream = On或者在代码中动态设置:
<?php
ini_set('xdebug.scream', '1');这在你怀疑某个关键错误被隐蔽的 @ 吞噬时非常有用。请谨慎将 scream 作为临时的调试工具使用,因为许多第三方库的代码可能在禁用 @ 的情况下无法正常运行。
延伸阅读:
1.4 ErrorException 桥接类
PHP 完全有能力成为一门“重异常”的编程语言,只需要几行代码就能完成转换。其核心思想是:利用 ErrorException 类(它继承自标准的 Exception 类),将 PHP 的原生“错误 (Errors)”捕获并抛出为“异常 (Exceptions)”。
这是大量现代框架(如 Symfony 和 Laravel)普遍采用的最佳实践。在调试模式下,这些框架会将错误转化为异常,并展示清晰、美观的调用堆栈跟踪 (stack trace)。
社区中也有像 Whoops! 这样优秀的开源包,专门用于提供极其友好的错误和异常展示界面。它是 Laravel 的默认组件,但也完全可以独立集成到任何框架中。
在开发阶段将错误抛出为异常,能让你更精细地处理它们。如果你在开发中遇到异常,你可以将其包裹在 catch 语句中,编写具体的补救逻辑。你捕获并处理的每一个异常,都会让你的应用程序变得更加健壮。
延伸阅读:
2. 异常 (Exceptions)
异常是大多数主流编程语言的标准组成部分,但在过去经常被 PHP 程序员忽视。像 Ruby 这样的语言极其依赖异常:HTTP 请求失败、数据库查询出错,甚至图片资源找不到,Ruby 都会抛出异常,让你立刻意识到哪里出了问题。
早期的 PHP 在这方面相对松散,调用 file_get_contents() 失败通常只会给你返回个 FALSE 顺带报个警告。老一代的 PHP 框架(如早期的 CodeIgniter)遇到错误也只是返回 false,悄悄把信息写进专有日志,然后让你调用类似 $this->upload->get_error() 的方法自己去查原因。这非常糟糕,因为你必须主动去“寻找”错误,还得查阅文档看这个类到底用哪个方法获取错误,而不是让错误主动、显眼地暴露出来。
另一个极端是,某些类在发生错误时直接往屏幕输出错误信息并用 exit 中止进程。这种做法剥夺了其他开发者在调用方动态处理该错误的权利。
最佳实践:发生错误时,类应该抛出异常以引起开发者的注意,然后将“如何处理这个错误”的决定权交给开发者。 例如:
<?php
$email = new Fuel\Email;
$email->subject('My Subject');
$email->body('How the heck are you?');
$email->to('guy@example.com', 'Some Guy');
try {
// 尝试发送邮件
$email->send();
} catch (Fuel\Email\ValidationFailedException $e) {
// 捕获并处理:数据验证失败的情况
echo "邮件地址格式不正确!";
} catch (Fuel\Email\SendingFailedException $e) {
// 捕获并处理:底层驱动无法发送的情况
echo "邮件服务器连接失败,请稍后再试!";
} finally {
// 无论是否抛出异常,这里的代码都会在正常执行恢复之前被执行
// 通常用于清理资源、关闭连接等
echo "邮件发送流程结束。";
}2.1 SPL 异常 (SPL Exceptions)
PHP 自带的通用 Exception 类能为开发者提供的调试上下文非常少。为了解决这个问题,我们可以通过继承基础 Exception 类来创建专用的异常类型:
<?php
// 自定义一个“验证异常”类
class ValidationException extends Exception {}这允许你使用多个 catch 块来精准拦截并分别处理不同类型的异常。
然而,这可能会导致你在项目中创建成百上千个自定义异常类。其实,很多时候你可以直接使用 PHP 标准库(SPL 扩展)中已经内置的丰富异常类,从而避免重复造轮子。
例如,如果你在类中使用了 __call() 魔术方法,当外部调用了一个不存在的动态方法时,与其抛出一个含糊不清的标准 Exception,或者专门为此去建一个自定义异常,你完全可以直接:
<?php
// 抛出 SPL 内置的“错误的方法调用异常”
throw new BadMethodCallException("您调用的方法不存在!");延伸阅读: