10. 错误与异常
错误
在很多「重异常(exception-heavy)」编程语言中,无论出现什么问题,都会抛出异常。这确实是一种可行的方法,但是 PHP 是「轻异常」的编程语言。虽然确实存在异常,并且很多核心在使用对象的时候已经开始使用这些异常了,但 PHP 更多的时候会尝试保持运行,除非发生了致命错误。
举个例子:
$ php -a
php > echo $foo;
Notice: Undefined variable: foo in php shell code on line 1
这只是一个提醒级别的错误,PHP 会继续运行。这会让那些来自「重异常」语言的开发者很困惑,因为例如在 Python 中,引用一个不存在的变量将抛出异常:
$ python
>>> print foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'foo' is not defined
唯一的区别是 Python 会在任何一件小事上终止程序,所以开发人员可以十分确认任何问题或边界值问题都会被捕获,而相同的情况 PHP 会继续进行。除非是发生极端问题,PHP 才会抛出错误并报告问题。
错误级别
PHP 有多个错误级别。三个最常见的错误级别是:错误、提醒、警告。这些错误的级别不同,E_ERROR
、E_NOTICE
和 E_WARNING
。「错误」 是运行时的致命错误,通常是代码错误导致的。由于会导致程序停止,所以需要及时修正。「提醒」 是因为代码而出现的建议消息,在执行期间不一定会导致问题,程序运行也不会停止。「警告」 是非致命错误,不会导致程序停止。
另一种错误信息是在编译时报告的 E_STRICT
。这些消息用于确保与即将发布的新版本 PHP 的兼容性,对代码的更改建议。
更改 PHP 的错误报告行为
错误报告可以通过 PHP 配置或 PHP 函数来更改。你可以在脚本执行期间,使用内置的 PHP 函数 error_reporting()
,传入一个表示错误级别的预定义常量,来设置错误报告级别。例如你只想要看到错误和警告,不想看到提醒,你可以这样设置:
<?php
error_reporting(E_ERROR | E_WARNING);
你还可以控制错误信息是否显示在屏幕上(适用于开发环境)或者是记录到日志(适用于生产环境)。这方面的更多信息你可以查看 错误报告 部分(原文)。
行内错误抑制
你可以使用 @
符号来告诉 PHP 抑制特定错误。你可以将此操作符放在表达式的开头,表达式导致的任何错误都将被抑制。
<?php
echo @$foo['bar'];
这行代码将会在值存在的时候输出 $foo['bar']
,但是当 $foo
或 bar
这个键不存在时,这句表达式会返回空值并且不输出任何内容。如果没有错误控制运算符,这个表达式会导致出现 PHP Notice: Undefined variable: foo
通知,或是 PHP Notice: Undefined index: bar
错误。
这似乎看上去不错,但也有一些不可取。PHP 处理带 @
符号的表达式时性能会收到影响。过早的性能优化可能在所有编程语言中都是争议点,但如果性能表现在你的应用/库中至关重要,那么理解错误抑制符对性能的影响就很重要了。
其次,错误控制操作符会完全吃掉错误。不但没有显示,而且也不会记录在错误日志中。此外,在正式环境中 PHP 也没有办法关闭错误控制操作符。也许你认为那些错误时无害的,不过那些较具伤害性的错误同时也会被隐藏。
如果有方法可以避免错误抑制符,你应该考虑避免使用。举个例子,上面的程序代码可以这样重写:
<?php
// 使用双问号运算符
echo $foo['bar'] ?? '';
当 fopen()
载入文件失败时,也许是一个使用错误抑制符的合理例子。你可以在尝试载入文件前检查是否存在,但是如果这个文件在检查后才被删除,而此时 fopen()
还未执行 (听起来有点不太可能,但是确实会发生),这时 fopen()
会返回 false 并且 抛出操作。这也许应该由 PHP 本身来解决,但这时一个错误抑制符才能有效解决的例子。
前面我们提到在正式的 PHP 环境中没有办法关闭错误控制操作符。但是 Xdebug 有一个 xdebug.scream
的 ini 配置项,可以关闭错误控制操作符。你可以按照下面的方式修改 php.ini
。
xdebug.scream = On
你也可以在运行时使用 ini_set
来设置。
<?php
ini_set('xdebug.scream', '1')
「Scream」这个 PHP 扩展提供了和 xDebug 类似的功能,只是 Scream 的 ini 设置项叫做 scream.enabled
。
当你在调试代码而错误信息被隐藏时,这是最有用的方法。请务必小心使用 scream ,仅把它当作暂时性的调试工具。有许多的 PHP 函数类库代码也许无法在错误抑制操作符停用时正常使用。
错误异常类
PHP 可以完美化身为「重异常」的程序语言,只需要几行代码就能切换过去。基本上你可以利用 ErrorException
类抛出「错误」来当做「异常」,这个类是继承自 Exception
类。
这在大量的现代框架中是一个常见的做法,比如 Symfony 和 Laravel。如果开启调试模式,或者进入开发环境的话,这两个框架都会将显示美观清晰的 调用栈。
还有一些可用的包可以更好地处理错误异常,像 Whoops! ,它随 Laravel 默认安装,也可以在任何框架中使用。
在开发过程中将错误当作异常抛出可以更好的处理它,如果在开发时发生异常,你可以将它包在一个 catch 语句中具体说明这种情况如何处理。每捕捉一个异常,都会使你的应用程序越来越健壮。
更多关于如何使用 ErrorException
来处理错误的细节,可以参考 ErrorException Class。
异常
异常是许多流行编程语言的标配,但它们往往被 PHP 开发人员所忽视。像 Ruby 就是一个极度重视异常的语言,无论有什么错误发生,像是 HTTP 请求失败,或者数据库查询有问题,甚至是图片资源丢失,Ruby (或是所使用的 gems),都将会抛出异常,你可以通过屏幕立刻知道所发生的问题。
PHP 处理这个问题则比较随意,调用 file_get_contents()
函数通常只会给出 FALSE
值和警告。许多较早的 PHP 框架比如 CodeIgniter 只是返回 false,将信息写入专有的日志,或者让你使用类似 $this->upload->get_error()
的方法来查看错误原因。这里的问题在于你必须找出错误所在,并且通过翻阅文档来查看这个类使用了什么样的错误的方法,而不是明确的暴露错误。
另一个问题发生在当类自动抛出错误到屏幕时会导致程序终止。这样做会妨碍其他开发者动态处理错误。正确的做法应该是抛出异常,让开发人员意识到错误的存在,让他们可以选择处理的方式,例如:
<?php
$email = new Fuel\Email;
$email->subject('这是邮件主题');
$email->body('你到底要怎样?');
$email->to('guy@example.com', '某个家伙');
try
{
$email->send();
}
catch(Fuel\Email\ValidationFailedException $e)
{
// 验证失败
}
catch(Fuel\Email\SendingFailedException $e)
{
// 邮件模块无法发送
}
finally
{
// 无论抛出什么样的异常都会执行,并且在正常程序继续之前执行
}
SPL 异常
原生的 Exception
类提供给开发人员的调试上下文不多。不过可以通过建立一个特殊的 Exception
来弥补它,方式就是建立一个继承自原生 Exception
类的子类:
<?php
class ValidationException extends Exception {}
这意味着你可以添加多个 catch 块,并根据不同的方式处理不同的异常。这可能会导致创建 过多 自定义异常,其中有部分已经在 SPL 扩展 中提供的 SPL 异常中定义了。
例如,如果你使用了 __call()
魔术方法,并且请求了一个无效的方法,那么你可以直接 throw new BadMethodCallException;
,而不是抛出一个模糊的标准 Exception ,或者为此创建一个自定义异常。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。