38

我一直在阅读特别的“错误日志”,我提出了“error_log”函数,它似乎是一个很好的工具来处理错误日志。但是如何最流畅、最好的使用它呢?

如果我有一个

try {
     //try a database connection...

} catch (PDOException $e) {
    error_log($e->getMessage(), 3, "/var/tmp/my-errors.log");

}

这会将错误记录在 my-errors.log 文件中。但是如果我有时需要更改文件的位置、新文件夹或其他东西的位置怎么办。如果我有大量文件,我需要全部更改。

现在我开始考虑使用一个变量来设置错误日志的路径。当然可以,但是如果我想在函数或类方法中使用 error_log 怎么办?然后我需要将变量设置为全局变量,但这被认为是不好的做法!但是如果我不应该在一个类的深处使用这个函数,那不也被认为是不好的做法吗?这里有什么好的解决方案?

<?php

function legit() {
    try {
        if (1 == 1) {
            throw new Exception('There was an error here');
        }
    } catch (Exception $e) {
        throw new Exception('throw the error to the try-catch outside the function...');
    }

}

try {
    legit();
} catch (Exception $e) {
    echo 'error here' . $e->getMessage();

    //log it
}

这是我上面所说的一个例子(没有在类/函数中深入记录......这是一个好方法吗?)

进一步:

我不太确定我应该如何使用一般的例外。假设我想在方法中使用 SQL 对数据库执行 INSERT,我会使用 try/catch,然后在失败时重新抛出异常吗?这被认为是好的做法吗?请举例。

4

11 回答 11

87

首先,我想赞扬您查看 PHP 中的标准错误方法。不幸的是error_log,您发现了一些限制。

这是一个很长的答案,请继续阅读以了解:

  1. 错误
    • 直接记录错误trigger_errorset_error_handler
    • 好的错误变坏的地方 - 致命错误。
  2. 例外
    • 声压级
    • 拿他们怎么办?
  3. 代码
    • 设置
    • 用法

TL;DR用于trigger_error引发错误并set_error_handler记录它们。

1. 错误

当程序中的事情没有按预期进行时,您通常会希望引发错误以便通知某人或某事。错误是指程序可能继续运行,但发生了值得注意的、可能有害或错误的情况。此时,许多人希望使用他们选择的日志记录包立即记录错误。我相信这是完全错误的做法。我建议使用trigger_error来引发错误,以便可以使用set_error_handler. 让我们比较这些选项:

直接记录错误

因此,您选择了您的日志记录包。现在,您可以在代码中出现错误的任何地方将调用传播到您的记录器。让我们看一下您可能拨打的单个电话(我将使用与 Jack 回答中的记录器类似的记录器):

Logger::getLogger('standard')->error('Ouch, this hurts');

你需要什么来运行这段代码?

    :记录器
    方法:getLogger
    返回:带有方法“错误”的对象

这些是使用此代码所需的依赖项。每个想要重用此代码的人都必须提供这些依赖项。这意味着标准的 PHP 配置将不再足以重用您的代码。在最好的情况下,使用依赖注入,您仍然需要将记录器对象传递到所有可能发出错误的代码中。

此外,除了代码负责的任何事情之外,它还负责记录错误。这违背了单一职责原则

我们可以看到直接记录错误是不好的

trigger_error 救援

PHP 有一个被称为函数的函数trigger_error,它可以像标准函数一样用来引发错误。与它一起使用的错误级别在错误级别常量中定义。作为用户,您必须使用用户错误之一:E_USER_ERRORE_USER_WARNING默认值E_USER_NOTICE(其他错误级别保留用于标准功能等)。使用标准 PHP 函数来引发错误允许代码在任何标准 PHP 安装中重复使用!我们的代码不再负责记录错误(只确保它被引发)。

使用trigger_error我们只执行一半的错误记录过程(引发错误)并将响应错误的责任留给接下来将介绍的错误处理程序。

错误处理程序

我们使用该函数设置了一个自定义错误处理程序set_error_handler(参见代码设置)。此自定义错误处理程序取代了标准 PHP 错误处理程序,后者通常根据 PHP 配置设置在 Web 服务器错误日志中记录消息。我们仍然可以通过false在自定义错误处理程序中返回来使用这个标准错误处理程序。

自定义错误处理程序只有一个职责:响应错误(包括您想要执行的任何日志记录)。在自定义错误处理程序中,您可以完全访问系统,并且可以运行您想要的任何类型的日志记录。几乎任何使用观察者设计模式的记录器都可以(我不打算深入讨论,因为我认为它是次要的)。这应该允许您挂接新的日志观察器以将输出发送到您需要的地方。

您可以完全控制在代码的单个可维护部分中对错误执行您喜欢的操作。现在可以在项目之间或在单个项目中从页面到页面快速轻松地更改错误记录。有趣的是,即使@被抑制的错误也会以 0 的值进入自定义错误处理程序errno,如果error_reporting尊重掩码,则不应报告。

当好的错误变坏时 - 致命错误

无法从某些错误继续。自定义错误处理程序无法处理以下错误级别:E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING。当这些类型的错误由标准函数调用触发时,自定义错误处理程序将被跳过并且系统关闭。这可以通过以下方式生成:

call_this_function_that_obviously_does_not_exist_or_was_misspelt();

这是一个严重的错误!无法恢复,系统即将关闭。我们唯一的选择是register_shutdown_function与关闭达成协议。但是,只要脚本完成(成功和不成功),就会执行此函数。当最后一个错误是致命错误时,使用这个和error_get_last一些基本信息可以记录(此时系统几乎关闭)。发送正确的状态代码并显示您选择的内部服务器错误类型页面也很有用。

2. 例外

可以以与基本错误非常相似的方式处理异常。trigger_error您的代码将抛出异常而不是异常(手动使用throw new Exception或来自标准函数调用)。用于set_exception_handler定义要用于处理异常的回调。

声压级

标准 PHP 库 (SPL) 提供了异常。它们是我引发异常的首选方式,因为trigger_error它们是 PHP 的标准部分,不会为您的代码引入额外的依赖项。

拿他们怎么办?

抛出异常时,可以进行三种选择:

  1. 抓住它并修复它(然后代码继续,好像没有发生任何不好的事情一样)。
  2. 抓住它,附加有用的信息并重新抛出它。
  3. 让它冒泡到更高的水平。

在堆栈的每一层,都会做出这些选择。最终,一旦它冒泡到最高级别,您设置的回调set_exception_handler将被执行。这是您的日志记录代码所属的地方(出于与错误处理相同的原因),而不是分布在catch代码中的整个语句中。

3.代码

设置

错误处理程序

function errorHandler($errno , $errstr, $errfile, $errline, $errcontext)
{
    // Perform your error handling here, respecting error_reporting() and
    // $errno.  This is where you can log the errors.  The choice of logger
    // that you use is based on your preference.  So long as it implements
    // the observer pattern you will be able to easily add logging for any
    // type of output you desire.
}

$previousErrorHandler = set_error_handler('errorHandler');

异常处理程序

function exceptionHandler($e)
{
    // Perform your exception handling here.
}

$previousExceptionHandler = set_exception_handler('exceptionHandler');

关机功能

function shutdownFunction()
{
    $err = error_get_last();

    if (!isset($err))
    {
        return;
    }

    $handledErrorTypes = array(
        E_USER_ERROR      => 'USER ERROR',
        E_ERROR           => 'ERROR',
        E_PARSE           => 'PARSE',
        E_CORE_ERROR      => 'CORE_ERROR',
        E_CORE_WARNING    => 'CORE_WARNING',
        E_COMPILE_ERROR   => 'COMPILE_ERROR',
        E_COMPILE_WARNING => 'COMPILE_WARNING');

    // If our last error wasn't fatal then this must be a normal shutdown.  
    if (!isset($handledErrorTypes[$err['type']]))
    {
        return;
    }

    if (!headers_sent())
    {
        header('HTTP/1.1 500 Internal Server Error');
    }

    // Perform simple logging here.
}

register_shutdown_function('shutdownFunction');

用法

错误

// Notices.
trigger_error('Disk space is below 20%.', E_USER_NOTICE);
trigger_error('Disk space is below 20%.'); // Defaults to E_USER_NOTICE

// Warnings.
fopen('BAD_ARGS'); // E_WARNING fopen() expects at least 2 parameters, 1 given
trigger_error('Warning, this mode could be dangerous', E_USER_WARNING);

// Fatal Errors.    
// This function has not been defined and so a fatal error is generated that
// does not reach the custom error handler.
this_function_has_not_been_defined();
// Execution does not reach this point.

// The following will be received by the custom error handler but is fatal.
trigger_error('Error in the code, cannot continue.', E_USER_ERROR);
// Execution does not reach this point.

例外

之前的三个选项中的每一个都在此处以通用方式列出,修复它,附加到它并让它冒泡。

1 可修复:

try
{
    $value = code_that_can_generate_exception();
}
catch (Exception $e)
{
    // We decide to emit a notice here (a warning could also be used).
    trigger_error('We had to use the default value instead of ' .
                  'code_that_can_generate_exception\'s', E_USER_NOTICE);
    // Fix the exception.
    $value = DEFAULT_VALUE;
}

// Code continues executing happily here.

2 附加:

看下面怎么code_that_can_generate_exception()不知道$context。此级别的 catch 块有更多信息,如果通过重新抛出它有用,它可以附加到异常中。

try
{
    $context = 'foo';
    $value = code_that_can_generate_exception();
}
catch (Exception $e)
{
    // Raise another exception, with extra information and the existing
    // exception set as the previous exception. 
    throw new Exception('Context: ' . $context, 0, $e);
}

3让它冒泡:

// Don't catch it.
于 2012-05-10T16:57:54.720 回答
37

已要求使此答案更适用于更大的受众,所以就这样吧。

前言

错误处理通常不是您在编写应用程序时首先要考虑的事情。作为间接结果,它会在需要时得到加强。但是,利用 PHP 中的现有机制也不必花费太多。

这是一篇相当长的文章,所以我把它分解成合乎逻辑的文本集。

触发错误

在 PHP 中有两种不同的触发错误的方式:

  1. PHP 本身的错误(例如使用未定义的变量)或内部函数(例如imagecreatefromjpeg无法打开文件),
  2. 用户代码使用触发的错误trigger_error

这些通常打印在您的页面上(除非display_errors关闭或error_reporting为零),这应该是生产机器的标准,除非您像我一样编写完美的代码......继续前进);set_error_handler这些错误也可以被捕获,让您可以通过稍后解释的使用来了解代码中的任何问题。

抛出异常

异常在三个主要方面不同于错误:

  1. 处理它们的代码可能远离它们被抛出的地方。源处的变量 state 必须显式传递给 Exception 构造函数,否则您只有堆栈跟踪。
  2. 异常和捕获之间的代码被完全跳过,而在发生错误(并且不是致命的)之后,代码仍然继续。
  3. 它们可以从主Exception类扩展;这允许您捕获和处理特定异常,但让其他人在堆栈中冒泡,直到它们被其他代码捕获。另见:http ://www.php.net/manual/en/language.exceptions.php

后面会给出一个抛出异常的例子。

处理错误

通过注册错误处理程序来捕获和处理错误非常简单,例如:

function my_error_handler($errno, $errstr, $errfile = 'unknown', $errline = 0, array $errcontext = array())
{
    // $errcontext is very powerful, it gives you the variable state at the point of error; this can be a pretty big variable in certain cases, but it may be extremely valuable for debugging
    // if error_reporting() returns 0, it means the error control operator was used (@)
    printf("%s [%d] occurred in %s:%d\n%s\n", $errstr, $errno, $errfile, $errline, print_r($errcontext, true));

    // if necessary, you can retrieve the stack trace that led up to the error by calling debug_backtrace()

    // if you return false here, the standard PHP error reporting is performed
}

set_error_handler('my_error_handler');

ErrorException对于踢,您可以通过注册以下错误处理程序(PHP >= 5.1)将所有错误转换为:

function exception_error_handler($errno, $errstr, $errfile, $errline)
{
    throw new ErrorException($errstr, $errno, 0, $errfile, $errline);
}

set_error_handler("exception_error_handler");

处理异常

在大多数情况下,您处理的异常尽可能接近导致它允许备份计划的代码。例如,您尝试插入数据库记录并抛出主键约束异常;您可以通过更新记录来恢复(设计为大多数数据库可以自己处理)。有些异常无法在本地处理,因此您希望将它们级联。例子:

function insertRecord($user, $name)
{
    try {
        if (true) {
            throw new Exception('This exception should not be handled here');
        }
        // this code is not executed
        $this->db->insert('users', array('uid' => $user, 'name' => $name));
    } catch (PDOException $e) {
        // attempt to fix; an exception thrown here will cascade down
        throw $e; // rethrow exception

        // since PHP 5.3.0 you can also nest exceptions
        throw new Exception("Could not insert '$name'", -1, $e);
    } catch (WhatEverException $e) {
        // guess what, we can handle whatever too
    }
}

滑溜的例外

那么当你在任何地方都没有捕捉到异常时会发生什么?您也可以使用set_exception_handler.

function my_exception_handler(Exception $exception)
{
    // do your stuff here, just don't throw another exception here
}

set_exception_handler('my_exception_handler');

除非您没有有意义的方法来处理代码中任何地方的异常,否则不鼓励这样做。

记录错误/异常

现在您正在处理错误,您必须在某处记录它。在我的示例中,我使用了一个 Apache 从 Java 移植到 PHP 的项目,名为LOG4PHP。还有其他的,但它说明了灵活的日志记录工具的重要性。

它使用以下概念:

  1. 记录器- 代表您执行记录的命名实体;它们可以特定于您项目中的某个类,也可以作为通用记录器共享,
  2. Appenders - 每个日志请求都可以根据预定义的条件(例如日志级别)发送到一个或多个目的地(电子邮件、数据库、文本文件),
  3. 级别 - 日志从调试消息分类到致命错误。

说明不同消息级别的基本用法:

Logger::getLogger('main')->info('We have lift off');
Logger::getLogger('main')->warn('Rocket is a bit hot');
Logger::getLogger('main')->error('Houston, we have a problem');

使用这些概念,您可以建模一个非常强大的日志记录工具;例如,在不更改上述代码的情况下,您可以实现以下设置:

  1. 收集数据库中的所有调试消息供开发人员查看;您可以在生产服务器上禁用此功能,
  2. 将警告收集到每日文件中,您可以在一天结束时通过电子邮件发送,
  3. 立即发送有关致命错误的电子邮件。
于 2012-05-07T04:20:20.210 回答
3

定义它,然后使用它:)

define('ERRORLOG_PATH', '/var/tmp/my-errors.log');

error_log($e->getMessage(), 3, ERRORLOG_PATH);

或者,只需将第三个参数error_log设为可选,将其默认为您想要的路径。

于 2012-04-26T12:02:42.313 回答
3

另外,对于错误日志(实际上是所有日志),我会使用事件调度器,就像 symfony 框架那样。

看看这个 sf 组件(它非常轻量级的依赖,不需要整个框架,可能有 3 个相关的 php 类和 2 个接口)

https://github.com/symfony/EventDispatcher

这样,您可以在应用程序引导程序中的某处创建调度程序:

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\Event;

$dispatcher = new EventDispatcher();

//register listeners
$dispatcher->addListener('application.log', function (Event $event) {
    //do anything you want
});

然后,您可以通过类似的方式在代码的任何位置引发事件

$dispatcher->dispatch(new GenericEvent('application.log', array('message' => 'some log', 'priority' => 'high'));

当然,您可以使用自己的事件子类化事件类:

class LogEvent extends GenericEvent {
    public function __construct($message, $priority = 'INFO') {
        parent::__construct('application.log', array('message'=>$message,'priority'=>$priority));
    }
    public function getMessage() { return $this->getArgument('message'); }
    public function getPriority() { return $this->getArgument('priority'); }
}

// now raising LogEvent is much cleaner:
$dispatcher->dispatch(new LogEvent('some log'));

这也将允许您创建更多自定义事件,例如 ExceptionEvent

 class ExceptionEvent extends GenericEvent {
    public function __construct(Exception $cause) {
        parent::__construct('exception.event', array('cause' => $cause));
    }
 }

并相应地处理它们。

优点

  • 您将日志记录逻辑与应用程序分开
  • 您可以在运行时轻松添加和删除记录器
  • 您可以轻松注册任意数量的记录器(即 DebugLogger 将所有内容记录到文本文件中,ErrorLogger 仅将错误记录到 error_log,CriticalLogger 仅记录生产环境中的严重错误并通过电子邮件将它们发送给管理员等)
  • 您可以将事件调度程序用于更多的事情,而不仅仅是记录(实际上对于每个适合观察者模式的工作)
  • 实际的记录器只不过是“实现细节”——它很容易替换,以至于你的日志去哪里都没有关系——你可以随时替换日志目的地,而不必重构你的方法的名称,或者改变任何东西在代码中。
  • 将很容易实现复杂的日志路由逻辑或全局更改日志格式(通过配置记录器)
  • 如果您对侦听器(记录器)和调度程序(进入通知日志事件的类)使用依赖注入,一切都会变得更加灵活

实际记录

正如有人已经说过的,我建议使用开箱即用的库,比如提到的 Monolog、Zend_Log 或 log4php,可能没有理由手动编写这些东西(你最不想要的就是损坏的错误记录器!)

PS:将代码片段视为伪代码,我没有测试它们。详细信息可以在提到的库的文档中找到。

于 2012-05-11T09:06:46.250 回答
3

如果您仍然需要处理日志的自定义方式(即您不想使用标准trigger_error()),我建议您查看 Zend_Log(http://framework.zend.com/manual/en/zend.log.overview。 html ) 出于以下原因:

  1. 这可以作为一个独立的组件使用,ZF 不是一个全栈框架。你可以只复制 Zend_Loader 和 Zend_Log 命名空间,实例化 Zend_Loader 并使用它。见下文:

    require_once('Zend/Loader/Autoloader.php');
    
    $loader = Zend_Loader_Autoloader::getInstance();
    
    $logger = new Zend_Log();    
    $writer = new Zend_Log_Writer_Stream('php://output');
    
    $logger->addWriter($writer);    
    $logger->log('Informational message', Zend_Log::INFO);
    
  2. 向您提供了许多日志库,但我相信 Zend 团队(PHP 语言的创始人)知道他们在做什么

  3. 您可以使用任何编写器(数据库、STDOUT - 见上文、文件等,您可以自定义它来编写您自己的以将日志消息发布到 Web 服务)
  4. 日志级别
  5. 可能会更改日志格式(但开箱即用的格式对我来说很棒)。上面带有标准格式化程序的示例将产生如下内容:

2012-05-07T23:57:23+03:00 信息 (6):信息性消息

  1. 只是阅读参考,它可能被配置为捕获 php 错误
于 2012-05-07T21:02:14.800 回答
0

有两个挑战要应对。首先是灵活地登录到不同的频道。在这种情况下,您应该看看例如Monolog

第二个挑战是将登录融入到您的应用程序中。恕我直言,最好的情况是不明确使用日志记录。例如,方面方向就派上用场了。一个很好的示例是flow3

但这更多是对问题的鸟瞰...

于 2012-05-10T08:50:40.047 回答
0

我使用我自己的函数,它允许我通过设置或更改第二个参数来编写多种类型的日志文件。

通过将 log 函数包含在我认为对我的开发项目“原生”的函数库中,我解决了您所问的关于“什么是正确的方法”的概念性问题。这样我就可以认为这些函数只是“我的”php核心的一部分,比如date()time()

在这个基本版本的 dlog 中,我还处理数组。虽然我最初使用它来记录错误,但我最终将它用于其他“快速而肮脏”的短期跟踪,例如记录代码进入某个部分的时间,以及用户登录等。

function dlog($message,$type="php-dlog")
{
    if(!is_array($message)  )
        $message=trim($message);
    error_log(date("m/d/Y h:i:s").":".print_r($message,true)."\n",3, "/data/web/logs/$_SERVER[HTTP_HOST]-$type.log");
} 
于 2012-05-11T03:36:44.360 回答
0

正如 KNL 所说,这是非常正确的,但不幸的是,到目前为止还没有记录,PHP 开发人员不推荐抛出异常的错误,并且有人在文档中犯了错误。它确实会导致许多扩展的错误,所以不要这样做。

这已经在 irc 上的#PHP 上进行了辩论。

“但是,错误可以简单地转换为带有 ErrorException 的异常。” 在http://php.net/manual/en/language.exceptions.php将被删除。

于 2013-04-17T02:14:49.600 回答
0

大多数错误记录器和异常记录器对大多数人来说都是无用的,因为它们无法访问日志文件。

我更喜欢使用自定义错误处理程序和自定义异常处理程序,并且如果系统在数据库上运行,则在生产期间将这些错误直接记录到数据库中。

在开发过程中,当设置了 display_errors 时,它们不会记录任何内容,因为所有错误都会在浏览器中引发。

作为旁注:不要让您的自定义错误处理程序抛出异常!这真是个坏主意。它可能会导致缓冲区处理程序和某些扩展出现错误。此外,一些核心 PHP 函数(如 fopen())会在失败时发出警告或通知,这些应该相应地处理,并且不应该停止应用程序有异常会做。

在 PHP 文档中提到让错误处理程序抛出异常是一个注释错误。

于 2013-04-08T05:38:50.927 回答
0

如果 PHP 处理错误的方式对您来说不够灵活(例如,有时您想记录到数据库,有时记录到文件,有时是其他),您需要使用/创建自定义 PHP 记录框架。

您可以浏览https://stackoverflow.com/questions/341154/php-logging-framework中的讨论,或者直接尝试首选KLogger。不过,我不确定它是否支持自定义日志记录目标。但至少,它是一个小而易读的类,您应该能够根据自己的需要进一步扩展它。

于 2012-05-01T07:31:57.457 回答
0

我会选择 Tom vand der Woerdt 的日志记录解决方案,它对您的要求最简单且最有效。

至于另一个问题:

您不需要在函数内捕获/重新抛出异常,除非您有解决方案的特定类型的异常。

有点简单的例子:

define('ERRORLOG_PATH', '/var/tmp/my-errors.log');
function do_something($in)
{
    if (is_good($in))
    {
        try {
            return get_data($in);
        } catch (NoDataException $e) {
            // Since it's not too big a deal that nothing
            // was found, we just return false.
            return false;
        }
    } else {
        throw new InvalidArguementException('$in is not good');
    }
}

function get_data($data)
{
    if (!is_int($data))
    {
        InvalidArguementException('No');
    }
    $get = //do some getting.
    if (!$get)
    {
        throw new NoDataException('No data was found.');
    } else {
        return $get;
    }
}

try {
   do_something('value');
} catch (Exception $e) {
   error_log($e->getMessage(), 3, ERRORLOG_PATH);
   die ('Something went wrong :(');
}

在这里,您只会捕获 ,NoDataException因为您有一些其他逻辑可以对其进行排序,所有其他错误都属于第一个捕获并由顶级捕获处理,因为所有抛出的异常都必须在其层次结构中的某个点继承自Exception.

显然,如果您Exception再次抛出(在初始try {}或顶部之外catch {}),您的脚本将退出并出现 Uncaught Exception 错误并且错误日志记录会丢失。

如果您想一路走下去,您还可以使用自定义错误处理功能set_error_handler()并将您的日志也放在那里。

于 2012-05-09T10:52:56.070 回答