什么时候应该使用PHP异常?

15

我看到很多教程演示了简单的try catch,例如打开文件。但我从未见过一个真正的大型实例。有人能给我提供一些他们已经或将要使用异常的情况吗?而且为了抛出异常,是否真的需要扩展异常类?最后,当抛出异常时,它会导致脚本 exit(); 吗?还是记录并继续执行脚本?


23
抛出可能重复异常,参见何时抛出异常 - Gordon
3
我希望您能提供一些关于PHP的特定回答,例如在许多教程中经常出现的类扩展。 - Josh
2
嗯,还有很多关于PHP异常的问答(http://stackoverflow.com/search?q=when+to+use+exceptions+php),至少你关于子类化和退出的问题在其中。 - Gordon
2
@Gordon,这个让我笑出声了。 - Shikiryu
7个回答

9
我们在项目中广泛使用异常。
一个特定的例子是对需要用户登录或注册的操作。我们使用异常来控制错误条件下的流程。如果当前用户未登录,我们会抛出一个异常。异常处理程序将重定向到登录页面。
以我们的注册操作为例,我们这样扩展异常:
class RegistrationFailed extends Exception {}

现在,在我们的注册代码中的catch语句中,我们可以测试RegistrationFailed异常并相应地处理它。否则,当异常不是RegistrationFailed时,我们允许其冒泡,因为我们对其不感兴趣。

try {
    // do registration here
}
catch(RegistrationFailed $e) {
    // handle the failed registration
}
catch(SomeOtherException $e) {
    // handle other errors like this...
}

// All other errors will not be caught and will bubble up

另一个例子是我们的包装类,开发者必须扩展它。我们使用反射来确保子类已经正确实现了它们的方法并提供了正确的接口。如果没有,我们通过异常通知该类的开发者,告诉他们子类必须提供特定的接口或方法。
编辑: 我已经听到关于“您不应该使用错误处理来控制流程!”的评论,然而,在上述项目中,这是必要的。
在程序的正常流程中,由于可能会失败的许多验证规则(例如密码过短),因此预期会出现注册失败的情况。
然而,它是一个ajax应用程序,因此当未登录时有人尝试手动访问ajax url时,这是一个异常情况,因此我们将其作为异常处理。

RegistrationFailed这个类实际上有任何内容吗?还是它只是一个命名的、默认的异常?是否有一种定义全局catch函数的方法?这样你就可以有十个try{}检查登录,但只有一个catch,可能靠近脚本的退出,这样你就不必不断地复制那段代码了。 - Josh
RegistrationFailed是一个空类。我们只是用它作为名称。我们有其他错误类,在其中向主体添加方法和数据。但大多数只是为了名称。我们从未使用全局catch函数,而是使用分叉的catch语句。我已经编辑了上面的内容以显示这一点。 - Jonathan Beebe
在你的try-catch块的结尾一定要使用一个通用的catch(Exception $e),因为这样你就可以处理之前没有处理过的其他异常。 - Sk8erPeter
3
我不同意Sk8erPeter对于捕获通用异常的看法。如果你不知道如何处理异常,就不应该捕获它。相反,应该让它向上传递到能够处理它的地方。如果没有任何东西能够处理它,那么你应该在顶层设置一个捕获并记录日志的东西,这样你就知道代码中有一个错误。 - David Peterson
@DavidPeterson 如果他正在使用像Symfony这样的PHP框架,那么应用程序将在顶层捕获它。 - AntonioCS

5

异常通常用于处理错误(至少在PHP中是这样)。假设您正在执行某个例程,发生了一个错误,而您无法在当前上下文中处理该错误。

例如:

<?php
/**
 * @throws Exception_NoFile
 */
function read_file($file) {
    if(!file_exists($file)) {
        throw new Exception_NoFile($file);
    }

    /* ... nominal case */
}

在这种情况下,由于没有要处理的文件,您不能继续使用名义案例。你必须选择:
  • 返回一个无效的返回值(这是C语言的惯例,例如:返回-1或使用状态标志)

  • 抛出异常,并希望有人能在上面捕获它。如果您的客户端代码期望它,那么没问题,它可以尝试另一条路径或重新抛出异常。如果您的客户端没有准备好处理请求的文件不存在的情况...您的代码将因为未捕获的异常而失败,就像在其他方法中读取不存在的文件时一样。


那么您不需要使用try{}catch{}吗?您可以随时抛出异常吗?是否有一种方法为每个异常定义一个通用的catch函数,以免重复编写catch{}?或者,在提供的情况下,您只需在那里退出脚本即可吗? - Josh
@Josh:如果你像我示例中所示那样直接抛出异常而没有使用try/catch,那么你必须在调用方设置一个try/catch块。例如:try {read_file("foo")} catch (Exception_NoFile $e){ /* 尝试其他操作 */} - erenon

5
异常处理是很棘手的。它需要仔细考虑所涉及的项目和错误处理方式。您应该尽早定义异常指南,并遵守它。我编写了一份通用的最佳实践 异常指南,经过广泛研究而得出。大多数指南可以适用于任何支持异常的语言中的所有项目。某些指南将是针对Java的。最后,您需要具备强大的指南集,以帮助您处理异常和错误条件。
以下是需要考虑的几点:

不要向客户公开内部实现细节

避免向客户公开内部实现特定的异常,特别是包含在第三方库中的异常。这是一个通用的面向对象原则,对于您的异常层次结构设计也同样适用。第三方库可能会更改其异常签名并破坏与客户端的API契约,而您无法控制第三方库。相反,将这些第三方异常(例如SQLException)封装在自己的自定义异常中。这样,您将拥有更大的灵活性,在不破坏客户端API契约的情况下,在未来更改第三方库。

为复杂的项目创建自己的异常层次结构

一般来说,为更复杂的模块创建自己的异常层次结构,特别是如果您正在处理第三方库中的实现特定异常。每个软件包/模块都可以拥有其自己的顶级通用异常。对于Java而言,至少应该定义一个继承RuntimeException的异常。将所有实现特定异常封装在自定义异常中,以便您的客户端仅依赖于您的自定义异常和/或通用Java异常。这将使您更容易重新设计实现特定代码,而不会破坏API契约。
如果需要更精细的错误处理,则可以进一步子类化自定义异常以处理特定情况并允许错误恢复。例如,如果要连接到SQL数据库,则可以以ConnectionTimeoutException的方式抛出异常,以使客户端在放弃之前可以重试N次连接。这样,您可以稍后更改数据库引擎为NoSQL,并仍然允许重新连接,而客户端代码将保持不变。

记录所有异常

在每个公共方法的javadoc定义中仔细记录软件包/模块/应用程序抛出的所有异常。未能这样做将使API用户感到沮丧,并导致他们不信任您的API文档。您真的不希望您的客户端挖掘您的源代码,只是为了找出您正在抛出特定异常,对吗?

尽早抛出异常

检查所有公共API方法的输入,并在发现预期参数与提供的参数之间存在不一致性时立即抛出异常。越早抛出异常,数据损坏的机会就越小,因为错误的数据不会进入您代码的更深层级。它也及时向您的客户提供有价值的反馈,而不是在您的代码深处,其中某些东西抛出一个带有糟糕消息(例如"内部错误"或"NullPointerException")的模糊异常。

适当地记录异常

按照您的日志框架指南记录异常及其消息和堆栈跟踪,以便正确记录。您不希望失去任何一个。


5

我觉得很多人把“失败”和“异常”混为一谈。“错误”这个词可以同时指二者,但我使用它来表示“失败”。

失败 - 当操作未成功时

异常 - 当出现意外或超出正常流程条件时

例如,如果一个机器人试图走到目的地却没有成功,那就是失败。但如果它断了腿或屋顶掉下来了,那就是异常。

如果屋顶掉下来了,我会抛出一个异常,说明屋顶掉下来了。

如果机器人没有到达目的地,我不会抛出异常,而是返回false或返回一个错误信息,比如“无法到达目的地,因为屋顶掉下来了。”

try {
  Walk to the cabinet;
}
catch (RoofFell_Exception $e) {
  return "Could not reach the destination because the roof fell.";
}
catch (Legbroke_Exception $e) {
  return "Could not reach the destination because a leg broke.";
}

if($current_location == 'cabinet') {
  return "Destination reached";
}
return false;

4
你应该看看Symfony框架 - 它们在那里真的使用了很多异常。
他们用异常来处理配置错误,比如你忘记把文件放在控制器期望找到它的地方 - 这将是一个异常,因为框架无法解决这个问题。
他们对于未知错误也使用异常:数据库由于某些奇怪的原因失败了,框架无法解决 - 因此它会抛出异常。
而且他们针对不同环境有不同的异常处理程序。当在“开发”模式下发生异常时,你会得到一个带有堆栈跟踪和解释的漂亮页面,当你处于“生产”模式时,你会被重定向到自定义的500页面。

0

在程序尝试连接或访问I/O(文件、数据库、网络、设备)时,使用异常处理是一个很好的选择。

  1. 当调用代码块(函数/方法)尝试访问文件时,请使用异常处理。

  2. 当调用代码块(函数/方法)尝试建立数据库连接时,请使用异常处理。

  3. 当调用代码块(函数/方法)尝试在数据库上运行查询时(任何尝试访问数据库表/视图等),请使用异常处理。

  4. 对于网络连接或访问也可以这样说。

内存访问需要I/O(包括将东西存储在$_SESSION文件中),但大多数初学者不会将整个程序放在try...catch结构的限制范围内。关于异常的使用和扩展Exception类的很好的例子可以在Matt Doyle的书《Beginning PHP 5.3》第20章652-60页中找到。

我还可以说,学习将异常处理与set_error_handler()trigger_error()error_log()的使用结合在catch块内,可以让您保留在开发中向输出设备(浏览器/stdout)回显的自定义、开发人员友好的错误消息。也就是说,在生产环境中,您的php.ini将关闭display_errors并打开log_errors。当连接到会计数据库失败时,您可能已经回显了类似于“糟糕,我无法连接到会计数据库”的内容,现在只需将相同的文本字符串发送到error_log(),您的个人错误消息仍然可以被记录。

示例:

function custom_handler($arg1, $arg2, $arg3, $arg4, $arg5)
{
  ...
  ...
  error_log(blah, blah, blah)
}

set_error_handler('custom_handler');  <--takes over this duty from PHP
$error = NULL;

try
{
     if(!connect_to_mythical_database('accounting'))
     {
          $error = 'I cannot connect to the accounting database';
          throw new Exception(supply-correct-arguments);
     }
}
catch (Exception $e)
{
     trigger_error(supply-correct-arguments); <-- does what 'custom_handler' instructs.
     error_log($error, blah, blah); <---Your friendly message here
     header('Location: http://www.myhomepage.com');
     exit;
}

注意:在custom_handler()内部,您可以使用error_log()将PHP错误消息记录在文件中、通过电子邮件发送或同时记录和发送标准的PHP错误消息。因此,trigger_error()要么由PHP(默认)控制,要么由您的“custom_handler”控制,后者可以实现error_log(),所有这些都可以通过异常处理程序的catch块激活。


0

异常仅仅是将边缘情况或错误(实际上只是大型边缘事件)从代码的主体中移出,以防它们使得99%的基本流程代码充斥着大量的开关/条件语句。

你可以将其看作一种反转的switch语句,其中try{}内部的事件确定发生哪个(如果有任何)catch块。

这意味着如果您不喜欢它们,则无需使用它们。但它们可以使代码更易于阅读。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接