如何最好地处理 PHP 页面上的错误?

38

目前我的页面看起来像这样:

if($_GET['something'] == 'somevalue')
{
    $output .= 'somecode';

    // make a DB query, fetch a row
    //...
    $row = $stmt->Fetch(PDO::ASSOC);

    if($row != null)
    {
        $output .= 'morecode';

        if(somethingIsOK())
        {
            $output .= 'yet more page output';
        }
        else
        {
            $error = 'something is most definitely not OK.';
        }
    }
    else
    {
        $error = 'the row does not exist.';
    }
}
else
{
    $error = 'something is not a valid value';
}

if($error == '') // no error
{
    //display $output on page
}
else // an error
{
    // display whatever error occurred on the page
}
我目前的做法可行,但对于显而易见的事情来说非常繁琐和乏味:比如在我的代码中间调用一个函数,或者想要检查一个变量的值,或者验证一个数据库查询是否返回了有效结果,如果它失败了,我想输出一个错误?我必须再做一个if/else块并将所有代码移动到新的if块内部。这似乎不是一种明智的做法。
我一直在阅读关于try/catch的内容,并考虑将我的所有代码放在try语句中,然后让代码依次运行,不需要任何if/else块,如果出现问题就抛出异常。根据我所读的内容,这会停止执行并直接跳转到catch块(就像失败的if语句会进入else块一样),在那里我可以输出错误消息。但是这是一种可接受或标准的做法吗?
在构建和输出HTML页面的PHP应用程序中处理错误(致命或非致命)的最佳方法是什么?我不想只是用空白屏幕死掉,因为那对用户很不友好,而是想在页面正文中输出一条消息,同时允许页眉和页脚显示。
感谢您的建议!

我自己使用 exit($error),例如:if($some_error == TRUE){exit('Error.');}希望这不是一个不好的做法。 - Abdulaziz
1
@AzizAG - 这不是个坏主意,但我的页面上有一个包装器,其中包括页眉和页脚,如果出现错误,我希望它以图形方式呈现。(即,我不想只是在一个空白的白色页面上死亡并显示错误消息)。 - Nate
@Nate 我撒谎了,我从未直接使用 exit。我有一个名为 exitApp($error) 的函数,它:(1)显示标题(2)回显 $error(3)显示页脚(4)最后退出应用程序。 - Abdulaziz
@AzizAG 这就是未捕获异常的作用,所以你可能只需要转换为异常。看看我的回答,它会向你展示如何启用 PDO 抛出异常以及如何将所有 PHP 错误转换为异常(PHP 专门为此目的提供了 ErrorException 类)。 - oxygen
8个回答

38

有很多方法可以处理这个问题,但实际上没有一种方法是本质上“正确”的。

你必须自己决定哪种方法更“舒适” - 这始终是个人偏好的问题(尽管有某些技术应该避免,出于很好的原因)。

这将高度取决于你如何分割你的逻辑,然而我倾向于将所有可能返回非致命错误的代码封装在一个函数内,并使用该函数的返回值来指示出现了错误。

对于致命错误,我倾向于使用异常(与try-catch块一起使用)。

现在只是为了明确:

  • 非致命错误是指你可以从中恢复的错误 - 意味着即使出现了问题,仍然可以执行一些代码并生成一些有价值的输出。例如,如果您想使用 NTP 协议获取当前时间,但服务器没有响应,您可以决定使用本地的 time 函数,并向用户显示一些有价值的数据。
  • 致命错误是指您无法从中恢复的错误 - 意味着发生了非常严重的问题,您唯一能做的就是告诉用户页面无法完成所要求的操作。例如,如果您从数据库中获取一些数据并得到了 SQL Exception,则没有有价值的数据可以显示,您只能告知用户此事。

非致命错误(使用函数返回值)

使用函数返回值来处理非致命问题的一个很好的例子是,当函数试图在页面上显示某个文件的内容时而这不是页面的主要目标(例如,你可能有一个函数,在每个页面上显示从文本文件中获取的徽章 - 我知道这有些牵强,但请容忍我)。

function getBadge($file){
    $f = fopen($file,'r');
    if(!$f){
        return null;
    }
    .. do some processing ..
    return $badges;
}

$badges = getBadges('badges.txt');
if(!$badges){
    echo "Cannot display badges.";
} else {
    echo $badges;
}
.. carry on doing whatever page should be doing ..

事实上,函数fopen本身就是一个例子 - 它将返回

成功时返回文件指针资源,错误时返回FALSE。


致命错误(使用异常 - try-catch)

当您有一些代码需要被执行,因为这正是用户想要的(例如从数据库中读取所有新闻并将它们显示给用户),您可以使用异常。让我们来看一个简单的例子 - 用户访问了他的个人资料并希望查看他收到的所有消息(现在先假设它们以纯文本形式存储)。您可能会有一个类似于以下的函数:

function getMessages($user){
    $messages = array();
    $f = fopen("messages_$user.txt","r");
    if(!$f){
        throw new Exception("Could not read messages!");
    }
    ... do some processing ...
    return $messages;
}

并像这样使用:

try{
    ..do some stuff..
    $messages = getMessages($_SESSION['user'])); //assuming you store username in $_SESSION
    foreach($messages as $msg){
        echo $msg."<br/>";
    }
} catch(Exception $e){
    echo "Sorry, there was an error: ".$e->getMessage();
}

现在这可能会很有用,如果你有一个“顶级”脚本,它将执行所有其他代码。这意味着,例如,在你的index.php中,你只需要:
try{
    .. execute some code, perform some functions ..
} catch(Exception $e){
    echo "Sorry, there was an error: ".$e->getMessage();
}

不要滥用异常!

无论做什么,都不要将异常用作检查可以恢复的内容的方式。请阅读另一个问题(完全归功于Anton Gogolev为此提供的非常好的解释以及其他回答者)以了解原因。

进一步阅读

现在没有比尝试几件事情并看看哪个对您有益更好的学习如何处理错误的方法了。 您可能会发现以下内容有用:

希望这有所帮助:)


顺便问一下,我想你是指 badges.txt 而不是 bages.txt - uınbɐɥs
@ShaquinTrifonoff:啊,确实是个打字错误 :) 感谢您的编辑! :) - Bart Platak
2
@norfavrell你真的在以PHP性能慢为理由避免使用异常吗?哈哈。我同意调试很烦,但我很少调试,因为我是PHPUnit的忠实粉丝。我不认为OP说的是正常的控制流,只是错误。即使是异常也会出错:JDBC:你曾经写过适合快速阅读的三重嵌套try-catch-finally结构吗? - pestilence669
我认为 if($badges){ echo $badges; } else { echo '无法显示徽章。'; } 会更好(去掉 ! 并交换消息)。 - uınbɐɥs
什么?“对于致命错误,我倾向于使用异常(带有try-catch块)。”未捕获的异常会变成致命错误。已捕获的异常不是致命的。任何其他致命错误都无法使用try-catch块处理。 - oxygen
显示剩余3条评论

6

PHP内置了一个类,ErrorException,用于将PHP错误翻译为异常,如果未处理,自然会停止执行。

异常具有改进的错误处理机制(try catch)和更好的调试信息(堆栈跟踪)。

将此包含在您的执行路径(配置或某些首先包含所有代码的内容)的最顶层:

 set_error_handler(function($nNumber, $strMessage, $strFilePath, $nLineNumber){
      throw new \ErrorException($strMessage, 0, $nNumber, $strFilePath, $nLineNumber);
 }, /*E_ALL*/ -1);

虽然PDO支持抛出异常,但默认情况下是关闭的,您需要启用它:

 $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);

如果使用MySQL,您还希望默认情况下可以原谅许多其他错误/警告,并且对于未设置必填字段的错误也会提示:
 $pdo->exec("SET sql_mode = 'STRICT_ALL_TABLES'");

异常可以像其他许多编程语言一样使用 try catch finally 来处理:

try
{
    echo $iAmAnUndefinedVariable;
}
catch(\Throwable $exception)
{
    /*[...]*/
}

在验证内容时,只需抛出异常:throw new Exception("Missing URL variable userId!");

如果PHP有一天能够干净利落地从传统的错误报告机制中脱离出来,并默认抛出异常(弃用error_reporting()并更改默认设置),那将是很好的。


6
这样做更加优雅易读。
try
{

    if($_GET['something'] != 'somevalue') 
    {
        throw new Exception ('something is not a valid value');
    }


    $output .= 'somecode';

    // make a DB query, fetch a row
    //...
    $row = $stmt->Fetch(PDO::ASSOC);

    if($row == null)
    {
        throw new Exception ('the row does not exist.');
    }


    $output .= 'morecode';


    if(somethingIsOK())
    {
        $output .= 'yet more page output';
    }
    else
    {
        throw new Exception ('something is most definitely not OK.');
    }


    echo $output;

}
catch (Exception $e)
{
    echo $e->getMessage();
}

1
这就是我在提问时所设想的,但我不确定它是否是try/catch的正确用法。 - Nate

3

使用 try-catch 是你可以使用的最干净的解决方案之一。

我制作了一个示例,当出现错误时仍显示标题和页脚,使用您的代码转换为 try-catch 格式:

PHP:

<?php
try {
    $output = array();
    if($_GET['something'] != 'somevalue') throw new Exception('something does not have a valid value.');
    $output[] = 'Some Code';
    $row = mt_rand(0, 10) < 5 ? null : mt_rand(0, 100);
    if($row === null) throw new Exception('The row does not exist.');
    $output[] = $row;
    if(!somethingIsOK()) throw new Exception('Something is most definitely not OK.');
    $output[] = 'Yet more page output';
} catch(Exception $e) {
    $output[] = 'Error: ' . $e->getMessage(); // To show output and error
    $output = array('Error: ' . $e->getMessage()); // To only show error
}
function somethingIsOK() {
    return mt_rand(0, 10) < 5;
}
?>

HTML:

<!DOCTYPE HTML>
<html lang="en-US">
<head>
    <meta charset="UTF-8" />
    <title>PHP Error test</title>
    <style type="text/css">
body {
    background: #eee;
    text-align: center
}
#content {
    padding: 60px
}
#header {
    padding: 30px;
    background: #fff
}
#footer {
    padding: 10px;
    background: #ddd
}
    </style>
</head>
<body>
    <div id="header">Header</div>
    <div id="content">
<?php echo implode('<br />', $output); ?>

    </div>
    <div id="footer">Footer</div>
</body>
</html>

参考文献:


1

对于查询,以及所有代码,都应该通过PDO错误异常处理来运行:

try{

}

catch{


}

finally{

}

这样做的原因是,当您可以大致确定错误发生在冗长脚本的哪个位置时,调试会变得更加容易。
更多信息请参见:http://php.net/manual/en/language.exceptions.php

4
我相信现在{}仅存在于RFC文档中,实际上无法使用。 - Erty Seidohl
这是为将来使用而准备的。对于原帖作者了解这个在未来的潜力是很有好处的。不过这是一个很好的观点。 - CodeTalk

0

通过使用错误处理函数正确处理PHP错误和警告。(请参见示例这里)

在PHP中处理错误的最佳方法是, 您可以通过在php文件顶部添加此行来停止所有错误报告 -

error_reporting(0);

//OR

error_reporting('E_ALL');

// Predefined Constant

使用函数在PHP中处理错误:

  • debug_backtrace — 生成回溯
  • debug_print_backtrace — 打印回溯
  • error_clear_last — 清除最近的错误
  • error_get_last — 获取最后发生的错误
  • error_log — 将错误消息发送到定义的错误处理程序
  • error_reporting — 设置报告哪些PHP错误
  • restore_error_handler — 恢复先前的错误处理程序函数
  • restore_exception_handler — 恢复先前定义的异常处理程序函数
  • set_error_handler — 设置用户定义的错误处理程序函数
  • set_exception_handler — 设置用户定义的异常处理程序函数
  • trigger_error — 生成用户级别的错误/警告/通知消息
  • user_error — trigger_error的别名

上述所有列出的函数都用于在PHP中处理错误。


0
创建错误处理程序(set_error_handler)并在其中抛出异常。
这将有助于那些不支持异常的函数。

-3
如果你正在寻找一种既美观又实用的代码结构,那么你可以使用我经常使用的“白名单”方法。例如,验证一个$_GET变量:
$error = false;

if(!isset($_GET['var'])) 
{
    $error = 'Please enter var\'s value';
}
elseif(empty($_GET['var'])) 
{
    $error = 'Var shouldn\'t be empty';
}
elseif(!ctype_alnum($_GET['var'])) 
{
    $error = 'Var should be alphanumeric';
}

//if we have no errors -> proceed to db part
if(!$error) 
{
    //inserting var into database table
}

所以,这就是它,只有两个if/elseif块,没有嵌套


我没有点踩,但是Nate正在寻找一些不需要if语句的东西,这样代码就需要添加最少量的代码来添加功能。 - uınbɐɥs
这就是 if/else 代码块的用途 - 用于检查变量和值。 - Noobie
是的,它们是,但这不是处理错误的有效方式。此外,这基本上就是@Nate已经拥有的。 - uınbɐɥs
@ShaquinTrifonoff,@Nate使用嵌套的if/else块进行错误检查并不好,这就是为什么我提出了这种方法;)干杯 - Noobie
@Nate 不想要 if/else 代码块。随着项目变得更加复杂,嵌套的代码块是不可避免的。因此,需要一个更清晰的解决方案。请查看被接受的答案和我的答案。更加简洁明了 - uınbɐɥs

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