Yii 的修行之路 - Errors(错误处理) & Logging(日志记录)
简述
这里简单归纳总结关于Yii的错误处理和日志记录的操作。
错误处理(Errors)
Yii 内置了一个yiiwebErrorHandler错误处理器,它使错误处理更方便, Yii错误处理器做以下工作来提升错误处理效果:
所有非致命PHP错误(如,警告,提示)会转换成可获取异常;
异常和致命的PHP错误会被显示,在调试模式会显示详细的函数调用栈和源代码行数。
支持使用专用的 控制器操作 来显示错误;
支持不同的错误响应格式;
yiiwebErrorHandler 错误处理器默认启用, 可通过在应用的入口脚本中定义常量YII_ENABLE_ERROR_HANDLER来禁用。
使用错误处理器
yiiwebErrorHandler 注册成一个名称为errorHandler应用组件, 可以在应用配置中配置它类似如下:
return [ 'components' => [ 'errorHandler' => [ 'maxSourceLines' => 20, ], ], ];
使用如上代码,异常页面最多显示20条源代码。
如前所述,错误处理器将所有非致命PHP错误转换成可获取异常,也就是说可以使用如下代码处理PHP错误:
use Yii; use yii\base\ErrorException; try { 10/0; } catch (ErrorException $e) { Yii::warning("Division by zero."); } // execution continues...
如果你想显示一个错误页面告诉用户请求是无效的或无法处理的,可简单地抛出一个 yiiwebHttpException异常, 如 yiiwebNotFoundHttpException。错误处理器会正确地设置响应的HTTP状态码并使用合适的错误视图页面来显示错误信息。
use yii\web\NotFoundHttpException; throw new NotFoundHttpException();
当错误处理器 捕获到一个异常,会从异常中提取状态码并赋值到响应
那么 yiiwebNotFoundHttpException 对应HTTP 404状态码,以下为Yii预定义的HTTP异常:
yiiwebBadRequestHttpException: status code 400.
yiiwebConflictHttpException: status code 409.
yiiwebForbiddenHttpException: status code 403.
yiiwebGoneHttpException: status code 410.
yiiwebMethodNotAllowedHttpException: status code 405.
yiiwebNotAcceptableHttpException: status code 406.
yiiwebNotFoundHttpException: status code 404.
yiiwebServerErrorHttpException: status code 500.
yiiwebTooManyRequestsHttpException: status code 429.
yiiwebUnauthorizedHttpException: status code 401.
yiiwebUnsupportedMediaTypeHttpException: status code 415.
如果想抛出的异常不在如上列表中,可创建一个yiiwebHttpException异常,带上状态码抛出,如下:
throw new \yii\web\HttpException(402);
日志(Logging)
Yii提供了一个强大的日志框架,这个框架具有高度的可定制性和可扩展性。使用这个框架,你可以轻松地记录各种类型的消息,过滤它们, 并且将它们收集到不同的目标,诸如文件,数据库,邮件。
使用Yii日志框架涉及下面的几个步骤:
在你代码里的各个地方记录 log messages;
在应用配置里通过配置 log targets 来过滤和导出日志消息;
检查由不同的目标导出的已过滤的日志消息(例如:Yii debugger)。
日志消息
记录日志消息就跟调用下面的日志方法一样简单:
Yii::trace():记录一条消息去跟踪一段代码是怎样运行的。这主要在开发的时候使用。
Yii::info():记录一条消息来传达一些有用的信息。
Yii::warning():记录一个警告消息用来指示一些已经发生的意外。
Yii::error():记录一个致命的错误,这个错误应该尽快被检查。
信息:日志消息可以是字符串,也可以是复杂的数据,诸如数组或者对象。log targets 的义务是正确处理日志消息。 默认情况下,假如一条日志消息不是一个字符串,它将被导出为一个字符串,通过调用:
yii\helpers\VarDumper::export()
为了更好地组织和过滤日志消息,我们建议您为每个日志消息指定一个适当的类别。 您可以为类别选择一个分层命名方案,这将使得log targets 在基于它们的分类来过滤消息变得更加容易。 一个简单而高效的命名方案是使用PHP魔术常量 METHOD 作为分类名称。这种方式也在Yii框架的核心代码中得到应用, 例如:
Yii::trace('start calculating average revenue', __METHOD__);
METHOD 常量计算值作为该常量出现的地方的方法名(完全限定的类名前缀)。
例如,假如上面那行代码在这个方法内被调用,则它将等于字符串 'appcontrollersRevenueController::calculate'。
日志目标
一个日志目标是一个 yiilogTarget 类或者它的子类的实例。它将通过他们的严重层级和类别来过滤日志消息,然后将它们导出到一些媒介中。
例如,一个 yiilogDbTarget 目标导出已经过滤的日志消息到一个数据的表里面,而一个 yiilogEmailTarget 目标将日志消息导出到指定的邮箱地址里。
消息过滤对于每一个日志目标,你可以配置它的 levels 和 categories 属性来指定哪个消息的严重程度和分类目标应该处理。
levels 属性是由一个或者若干个以下值组成的数组:
error:相应的消息通过 Yii::error() 被记录。
warning:相应的消息通过 Yii::warning() 被记录。
info:相应的消息通过 Yii::info() 被记录。
trace:相应的消息通过 Yii::trace() 被记录。
profile:相应的消息通过 Yii::beginProfile() 和 Yii::endProfile() 被记录。更多细节将在 Profiling 分段解释。
如果你没有指定 levels 的属性, 那就意味着目标将处理 任何 严重程度的消息。
如果你没有指定 categories 属性,这意味着目标将会处理 任何 分类的消息。
消息格式化
日志目标以某种格式导出过滤过的日志消息。例如,假如你安装一个 FileTarget 类的日志目标, 你应该能找出一个日志消息类似下面的 runtime/log/app.log 文件:
2014-10-04 18:10:15 [::1][][-][trace][yii\base\Module::getModule] Loading module: debug
你可以通过配置 yiilogTarget::prefix 的属性来自定义格式,这个属性是一个PHP可调用体返回的自定义消息前缀。例如,下面的代码配置了一个日志目标的前缀是每个日志消息中当前用户的ID(IP地址和Session ID被删除是由于隐私的原因)。
[ 'class' => 'yii\log\FileTarget', 'prefix' => function ($message) { $user = Yii::$app->has('user', true) ? Yii::$app->get('user') : null; $userID = $user ? $user->getId(false) : '-'; return "[$userID]"; } ]
除了消息前缀以外,日志目标也可以追加一些上下文信息到每组日志消息中。
你可以通过配置 logVars 属性适应这个行为,这个属性是你想要通过日志目标包含的全局变量名称。
举个例子,下面的日志目标配置指明了只有 $_SERVER 变量的值将被追加到日志消息中:
[ 'class' => 'yii\log\FileTarget', 'logVars' => ['_SERVER'], ]
你可以将 logVars 配置成一个空数组来完全禁止上下文信息包含。或者假如你想要实现你自己提供上下文信息的方式, 你可以重写 yiilogTarget::getContextMessage() 方法。
日志写入的案例
1、在具体的控制器上进行日志收集处理,调用相应的日志接口即可。
其中传参需要注意,日志内容($content)和控制器方法路径(魔术常量 __METHOD__)需要保证必传。
2、对应日志处理接口处理,实现代码封装。
日志收集接口代码封装需要注意几个点:
(1)组装日志内容需要按照与团队内部约定好的格式进行拼接组装。
(2)确定好日志文件路径($logFile),和文件命名规则,因为日志可能是实时,可能需要生产多个日志文件。
(3)日志写入内容时判断日志文件是否存在(是否需要新建),写入时利用 flock() 进行文件独占锁定(目的是避免同时操作同一份日志),利用 clearstatcache() 函数清除文件状态,再日志内容写入日志文件。