第十二章 调试,日志和错误处理
在本章中,我们将会讨论如下话题:
- 使用不同的日志路由
- 分析Yii错误堆栈踪迹
- 打日志并使用上下文信息
- 展示自定义错误
- 为调试扩展自定义面板
介绍
如果应用比较复杂,创建一个没有bug的应用几乎是不可能的,所以开发者必须检测错误并能迅速的处理他们。Yii有一套实用的特性,可以处理日志和错误。而且,在调试模式下,如果发生错误,Yii可以给出堆栈踪迹。实用它,你可以非常迅速的修复错误。
在本章中,我们将会回顾日志,分析异常的堆栈踪迹,并实现自己的错误处理。
使用不同的日志路由
当你没有机会调试它的时候,打日志对于理解应用真正做了些什么非常关键。不管你是否相信,尽管你能100%确信你的应用将会按照你期望的执行,在生产环境中,它可以做很多你意识不到的事情。这没关系,因为没有人可以注意到任何事情。因此,如果我们期望不寻常的行为,我们需要立刻知道并有足够的信息来重现它。这就是日志派上用场的原因。
Yii允许一个开发者不止可以输出日志消息,也能根据消息的级别和种类进行不同的处理。例如,你可以将一条消息写入到数据库,发送一个电子邮件或者将它展示到浏览器中。
在本小节中,我们将会以更明智的方法处理日志消息:最重要的信息通过邮件发送,不太重要的信息会被保存到的文件A和B中,profiling将会被路由到Firebug中。此外,在开发模式下,所有的消息和profiling信息将会展示在屏幕上。
准备
按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
如何做…
执行如下步骤:
- 使用
config/web.php配置日志:
'components' => ['log' => ['traceLevel' => 0,'targets' => [['class' => 'yii\log\EmailTarget','categories' => ['example'],'levels' => ['error'],'message' => ['from' => ['log@example.com'],'to' => ['developer1@example.com','developer2@example.com'],'subject' => 'Log message',],],['class' => 'yii\log\FileTarget','levels' => ['error'],'logFile' => '@runtime/logs/error.log',],['class' => 'yii\log\FileTarget','levels' => ['warning'],'logFile' => '@runtime/logs/warning.log',],['class' => 'yii\log\FileTarget','levels' => ['info'],'logFile' => '@runtime/logs/info.log',],],],'db' => require(__DIR__ . '/db.php'),],
- 现在,我们会在
protected/controllers/LogController.php中生成一些消息:
<?phpnamespace app\controllers;use yii\web\Controller;use Yii;class LogController extends Controller{public function actionIndex(){Yii::trace('example trace message', 'example');Yii::info('info', 'example');Yii::error('error', 'example');Yii::trace('trace', 'example');Yii::warning('warning','example');Yii::beginProfile('preg_replace', 'example');for($i=0;$i<10000;$i++){preg_replace('~^[ a-z]+~', '', 'test it');}Yii::endProfile('preg_replace', 'example');return $this->render('index');}}
以及视图views/log/index.php:
<div class="log-index"><h1>Log</h1></div>
- 现在多次运行先前的动作。在屏幕上,你应该看到
Log头和一个有日志消息数字的调试面板:

- 如果你点击17,你将会看到一个web日志,如下图所示:

- 一条日志包含我们打的所有信息,堆栈踪迹、时间戳、级别和分类。
- 现在打开Profiling页面。你应该能看到profiling消息,如下截图所示:

profiling信息展示了我们代码块的所有执行时长。
- 因为我们刚刚修改了日志文件的名称,而不是路径,你应该能在
runtime/logs中找到日志文件error.log、warning.log和info.log。 - 打开文件你将会看到如下消息:
2016-03-06 07:28:35 [127.0.0.1][-][-][error][example] error...2016-03-06 07:28:35 [127.0.0.1][-][-][warning][example] warning...2016-03-06 07:28:35 [127.0.0.1][-][-][info][example] info
工作原理…
当使用Yii::erorr、Yii::warning、Yii::info或者Yii::trace打日志时,Yii将它传递给了日志路由。
依赖于如何配置,它会将消息发送给一个或多个目标,例如,通过电子邮件发送错误信息、将调试信息写入到文件A中、将警告信息写入到文件B中。
yii\log\Dispatcher类的对象通常被附加在一个名叫log的应用组件上。因此,为了配置它,我们应该在配置文件组件部分设置它的属性。这里唯一可配的属性是targets,它包含了一组日志路由和他们的配置。
我们已经定义了四个日志路由。这里回顾一下:
['class' => 'yii\log\EmailTarget','categories' => ['example'],'levels' => ['error'],// 'mailer' => 'mailer','message' => ['from' => ['log@example.com'],'to' => ['developer1@example.com', 'developer2@example.com'],'subject' => 'Log error',],],
EmailTarget默认通过Yii::$app->mailer组件发送一封电子邮件来发送日志消息。我们将类别限制为example,并将级别限制为error。电子邮件将会从log@example.com发送给两个开发者,且主题是Log error:
['class' => 'yii\log\FileTarget','levels' => [warning],'logFile' => '@runtime/logs/warning.log',],
FileTarget将错误消息发送到一个指定的文件。我们将消息级别限制为warning,并使用一个名叫warning.log的文件。同样我们将info级别的消息存放在Info.log文件中。
此外,我们可以使用yii\log\SyslogTarget将消息写到Unix /var/log/syslog系统文件中,或者使用yii\log\DbTarget将消息写入到数据库中。对于第二种情况,你必须应用他们的migration:
./yii migrate --migrationPath=@yii/log/migrations/
更多…
关于Yii打日志有很多有趣的东西,在接下来的部分中进行讨论。
Yii::trace和Yii::getLogger()->log
Yii::trace是对Yii::log的封装:
public static function trace($message, $category = 'application'){if (YII_DEBUG) {static::getLogger()->log($message, Logger::LEVEL_TRACE, $category);}}
因此,如果Yii在debug模式下,Yii::trace使用trace级别来打日志。
Yii::beginProfile和Yii::endProfile
这些方法被用于测量应用中部分代码的执行时间。在我们的LogController中,我们测量了preg_replace执行10000次所用的时间:
Yii::beginProfile('preg_replace', 'example');for($i=0;$i<10000;$i++){preg_replace('~^[ a-z]+~', '', 'test it');}Yii::endProfile('preg_replace', 'example');
Yii::beginProfile标记用于profiling的代码块开头。我们必须为每一个代码块设置一个唯一的token,以及指定一个可选的分类:
public static function beginProfile($token, $category = 'application') { … }
Yii::endProfile可以匹配到先前调用有相同的分类名的beginProfile:
public static function endProfile($token, $category = 'application') { … }
begin-和end-调用也必须被正确的嵌套。
立即打日志消息
默认情况下,Yii会将所有的日志消息存放在内存中,知道应用终止。这是为了性能考虑,并且一般都能运行良好。
但是,如果一个控制台应用需要长时间运行,日志消息将不会被立刻写出。为了确保你的消息能在任何时候都被打印出来,你可以使用Yii::$app->getLogger()>flush(true)显式刷新,或者为你的控制台应用配置修改flushInterval和exportInterval:
'components' => ['log' => ['flushInterval' => 1,'targets' =>[['class' => 'yii\log\FileTarget','exportInterval' => 1,],],],],
参考
- 欲了解更多关于打日志的信息,参考http://www.yiiframework.com/doc-2.0/guideruntime-logging.html
- 日志和使用上下文信息小节
分析Yii错误堆栈踪迹
当发生错误时,Yii可以展示错误信息以及错误堆栈踪迹。当我们需要知道究竟是什么原因导致的错误时,堆栈踪迹非常有用。
准备
- 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
- 配置一个数据库,并使用如下migration导入:
<?phpuse yii\db\Migration;class m160308_093234_create_article_table extends Migration{public function up(){$this->createTable('{{%article}}', ['id' => $this->primaryKey(),'alias' => $this->string()->notNull(),'title' => $this->string()->notNull(),'text' => $this->text()->notNull(),]);}public function down(){$this->dropTable('{{%article}}');}}
- 使用Yii生成一个
Article模型。
如何做…
执行如下步骤:
- 现在我们需要创建一些代码。创建
protected/controllers/ErrorController.php:
<?phpnamespace app\controllers;use app\models\Article;use yii\web\Controller;class ErrorController extends Controller{public function actionIndex(){$article = $this->findModel('php');return $article->title;}private function findModel($alias){return Article::findOne(['allas' => $alias]);}}
- 运行过先前的动作以后,我们应该能得到如下错误:

- 而且,堆栈踪迹展示了如下错误:

工作原理…
从错误消息中,我们知道在数据库中,我们没有列的别称,但是我们已经再代码别的地方用到了它。在我们的例子中,这很容易通过搜索所有的文件来找到,但是在一个大项目中,一个列可以存放在一个变量中。而且,we have everything to fix an error without leaving the screen where the stack trace is displayed。我们只是需要小心的读它。
堆栈踪迹逆序地展示了一个调用链条,以产生错误的一个开始。一般来说,我们不需要看所有的踪迹来了解发生了什么。这个框架代码本身已经充分测试了,所以发生错误的可能性是比较小的。这就是为什么Yii展示应用的踪迹是打开的,而框架的踪迹是折叠的。
因此,我们使用第一个展开的部分,并查找别称。找到以后,我们可以立刻告诉你它被用于ErrorController.php的第19行中。
参考
- 欲了解错误处理的信息,参考http://www.yiiframework.com/doc-2.0/guide-runtime-handling-errors.html
- 日志和使用上下文信息小节
日志和使用上下文信息
有时,一个错误信息不足以修复一个错误。例如,如果你使用最佳实践,并且使用所有可能的错误来开发和测试一个应用,你可以得到一个错误信息。但是,没有执行的上下文,它只是告诉你这里有一个错误,并不清楚究竟是什么导致的。
在我们的例子中,我们将会使用一个非常简单并且代码编写很烂的动作,它会输出Hello <username>!,其中username直接从$_GET中获取。
准备
按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
如何做…
执行如下步骤:
- 首先,我们需要一个控制器。因此,创建
protected/controllers/LogController.php:
<?phpnamespace app\controllers;use yii\web\Controller;class LogController extends Controller{public function actionIndex(){return 'Hello, ' . $_GET['username'];}}
- 现在,如果我们运行index动作,我们将会得到一个错误信息,
Undefined index: username。配置logger将这样的错误写入到文件:
config/web.php
'components'=>[...'log' => ['targets' => [['class' => 'yii\log\FileTarget','levels' => ['error'],'logFile' => '@runtime/logs/errors.log',],],],],
- 在次运行index动作,并检查
runtime/logs/errors.log。将会有如下日志信息:
2016-03-06 09:27:09 [127.0.0.1][-][-][error][yii\base\ErrorException:8] exception 'yii\base\ErrorException' withmessage 'Undefined index: username' in /controllers/LogController.php:11Stack trace:#0 /yii2/base/InlineAction.php(55): ::call_user_func_array()#1 /yii2/base/Controller.php(151): yii\base\InlineAction->runWithParams()#2 /yii2/base/Module.php(455): yii\base\Controller->runAction()#3 /yii2/web/Application.php(84): yii\base\Module->runAction()#4 /yii2/base/Application.php(375): yii\web\Application->handleRequest()#5 /web/index.php(12): yii\base\Application->run()#6 {main}2016-03-06 09:27:09 [127.0.0.1][-][-][info][application] $_GET= ['r' => 'log/index']$_COOKIE = ['_csrf' => 'ca689043348e...a69ea:2:{i:0;s:...\"DSS...KJ\";}''PHPSESSID' => '30584oqhat4ek8b0hrqsapsbf4']$_SERVER = ['USER' => 'www-data''HOME' => '/var/www''FCGI_ROLE' => 'RESPONDER''QUERY_STRING' => 'r=log/index'...'PHP_SELF' => '/index.php''REQUEST_TIME_FLOAT' => 1459934829.3067'REQUEST_TIME' => 1459934829]
- 现在我们可以将我们的应用给一个测试组并不时的检查错误日志。默认情况下,错误报告日志包含了
$_GET、$_POST、$_FILES、$_COOKIE、$_SESSION、$_SERVER变量中的所有的值。如果你不希望展示所有的值,你可以指定一个自定义的变量列表:
'log' => ['targets' => [['class' => 'yii\log\FileTarget','levels' => ['error'],'logVars' => ['_GET', '_POST'],'logFile' => '@runtime/logs/errors.log',],],],
- 在这个例子中,报告只包含
$_GET和$_POST两个数组:
...2016-04-06 09:49:08 [127.0.0.1][-][-][info][application] $_GET= [ 'r' => 'log/index' ]
工作原理…
Yii在打印错误日志信息时,添加了执行上下文和环境的完整信息。如果我们手动打一个日志消息,我们可能知道我们需要的信息,所以我们可以设置一些目标选项来我们真正需要的东西:
'log' => ['targets' => [['class' => 'yii\log\FileTarget','levels' => ['error'],'logVars' => ['_GET', '_POST'],'logFile' => '@runtime/logs/errors.log',],],],
先前的代码会将错误日志写到一个名叫errors的文件中,此外对于消息本身,它会将$_GET和$_POST变量的内容打到日志中,如果这两个变量不为空的话。
参考
- 欲了解更多关于日志过滤器和上下文信息,参考http://www.yiiframework.com/doc-2.0/guide-runtime-logging.html
- 使用不同的日志路由小节
展示自定义错误
在Yii中,错误处理是非常灵活的,所以你可以为一种特定的错误创建你自己的错误处理方法。在这个小结中,我们将会以一个非常灵敏的方法处理一个404找不到的错误。我们将会展示一个404页面,它会基于输入栏中输入的内容提供建议的内容。
准备
- 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
- 添加失败动作到你的
SiteController:
class SiteController extends Controller{// …public function actionFail(){throw new ServerErrorHttpException('Error message example.');}}
- 添加如下内容到
web/.htaccess:
RewriteEngine onRewriteCond %{REQUEST_FILENAME} !-fRewriteCond %{REQUEST_FILENAME} !-dRewriteRule . index.php
- 在
config/web.php文件中为urlManager组件配置友好的URL:
'components' => [// …'urlManager' => ['enablePrettyUrl' => true,'showScriptName' => false,],],
- 对于不存在的URL,展示
Not found异常:

- 同时,在我们的
actionFail中展示Internal Server Error异常:

- 现在我们希望为
Not Found页面创建一个自定义页面。
如何做…
现在我们需要修改Not Found页面的内容,但不考虑其它错误类型。为了达到这个目标,执行如下步骤:
- 打开
SiteController类并找到actions()方法:
class SiteController extends Controller{// ...public function actions(){return ['error' => ['class' => 'yii\web\ErrorAction',],'captcha' => ['class' => 'yii\captcha\CaptchaAction','fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,],];}// ...}
- 移除默认的
error部分,actions()如下所示:
<?phpclass SiteController extends Controller{// ...public function actions(){return ['captcha' => ['class' => 'yii\captcha\CaptchaAction','fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,],];}// ...}
- 添加自己的
actionError()方法:
class SiteController extends Controller{// ...public function actionError(){}}
- 打开原始的
\yii\web\ErrorAction类,复制它的动作内容到我们的actionError()中,并自定义它用于渲染自定义error-404视图,从而展示404错误码的Not Found错误:
<?php// ...use yii\base\Exception;use yii\base\UserException;class SiteController extends Controller{// ...public function actionError(){if (($exception =Yii::$app->getErrorHandler()->exception)== null) {$exception = new HttpException(404, Yii::t('yii','Page not found.'));}if ($exception instanceof HttpException) {$code = $exception->statusCode;} else {$code = $exception->getCode();}if ($exception instanceof Exception) {$name = $exception->getName();} else {$name = Yii::t('yii', 'Error');}if ($code) {$name .= " (#$code)";}if ($exception instanceof UserException) {$message = $exception->getMessage();} else {$message = Yii::t('yii', 'An internal server error occurred.');}if (Yii::$app->getRequest()->getIsAjax()) {return "$name: $message";} else {if ($code == 404) {return $this->render('error-404');} else {return $this->render('error', ['name' => $name,'message' => $message,'exception' => $exception,]);}}}}
- 使用一个自定义消息添加
views/site/error-404.php视图文件:
<?phpuse yii\helpers\Html;/* @var $this yii\web\View */$this->title = 'Not Found!'?><div class="site-error-404"><h1>Oops!</h1><p>Sorry, but requested page not found.</p><p>Please follow to <?= Html::a('index page', ['site/index'])?>to continue reading. Thank you.</p></div>
- 现在尝试访问不存在的URL,就能看到
error-404.php视图中的内容:

- 但是,对于一个失败的动作,我们能看到
error.php文件中默认的内容:

工作原理…
默认情况下,在yii2-app-basic应用中,我们在配置文件config/web.oho中为errorHandler组件配置errorAction为site/error。这意味着这个框架将会使用这个路由用于展示每一个被处理的异常:
'components' => ['errorHandler' => ['errorAction' => 'site/error',],],
在SiteController类中,我们使用内置的yii\web\ErrorAction类,它会渲染所谓的error.php视图:
class SiteController extends Controller{// ...public function actions(){return ['error' => ['class' => 'yii\web\ErrorAction',],'captcha' => ['class' => 'yii\captcha\CaptchaAction','fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,],];}// ...}
如果我们希望复写它的实现,我们可以 replace it in an inline actionError() method with our own custom content。
在这个小结中,我们添加了自己的if条件,用于渲染一个基于错误码的指定视图:
if ($code == 404) {return $this->render('error-404');} else {return $this->render('error', ['name' => $name,'message' => $message,'exception' => $exception,]);}
同时,我们可以为Not Found页面使用一个自定义设计。
参考
为了了解更多Yii中的错误处理,参考http://www.yiiframework.com/doc-2.0/guide-runtime-handling-errors.html。
为调试扩展自定义面板
Yii2-debug扩展是一个强大的工具,可以调试自己的代码,分析请求信息或者数据库查询等等。因此,你可以添加你自己的面板用于任何自定义报告。
准备
按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。
如何做…
- 在你的网站根路径中创建
panels目录: - 添加一个新的
UserPanel类:
<?phpnamespace app\panels;use yii\debug\Panel;use Yii;class UserPanel extends Panel{public function getName(){return 'User';}public function getSummary(){return Yii::$app->view->render('@app/panels/views/summary', ['panel' => $this]);}public function getDetail(){return Yii::$app->view->render('@app/panels/views/detail', ['panel' => $this]);}public function save(){$user = Yii::$app->user;return !$user->isGuest ? ['id' => $user->id,'username' => $user->identity->username,] : null;}}
- 使用如下代码创建
panels/view/summary.php:
<?php/* @var $panel app\panels\UserPanel */use yii\helpers\Html;?><div class="yii-debug-toolbar__block"><?php if (!empty($panel->data)): ?><a href="<?= $panel->getUrl() ?>">User<span class="yii-debug-toolbar__label yii-debug-toolbar__label_info"><?= Html::encode($panel->data['username']) ?></span></a><?php else: ?><a href="<?= $panel->getUrl() ?>">Guest session</a><?php endif; ?></div>
- 使用如下代码创建
panels/view/detail.php视图:
<?php/* @var $panel app\panels\UserPanel */use yii\widgets\DetailView;?><h1>User profile</h1><?php if (!empty($panel->data)): ?><?= DetailView::widget(['model' => $panel->data,'attributes' => ['id','username',]]) ?><?php else: ?><p>Guest session.</p><?php endif;?>
- 在配置文件
config/web.php中打开你的工具栏:
if (YII_ENV_DEV) {$config['bootstrap'][] = 'debug';$config['modules']['debug'] = ['class' => 'yii\debug\Module','panels' => ['views' => ['class' => 'app\panels\UserPanel'],],];$config['bootstrap'][] = 'gii';$config['modules']['gii'] = 'yii\gii\Module';}
- 重新加载
index页面,并在调试面板的末尾寻找游客Session:

- 使用
admin用户名和admin密码登录到你的站点。在一个成功的例子中,你可以在主菜单中看到你的用户名:

- 再次浏览调试面板。现在,你将会看到
admin用户名:

- 你可以点击调试面板中的用户名,并看到详细的用户信息:

工作原理…
为了给yii2-debug模块创建我们自己的面板,我们需要扩展yii\debug\Panel类,并复写自己的模板方法;
getName():调试详情页的菜单项标签名getSummary():调试面板格代码getDetail():详细页视图代码save():你的信息,将会被保存的调试存储中,并从$panel->data字段中获取
你的对象可以存储任何调试数据,并在摘要块和详情页中展示:
在我们的例子中,我们存储用户信息:
public function save(){$user = Yii::$app->user;return !$user->isGuest ? ['id' => $user->id,'username' => $user->identity->username,] : null;}
在摘要和详情页展示$panel->data字段中的数据。
处理事件
你可以订阅应用的任何事件,或者init()方法中的任何组件。例如,内置yii\debug\panels\MailPanel面板搜集和存储所有被发送的信息:
class MailPanel extends Panel{private $_messages = [];public function init(){parent::init();Event::on(BaseMailer::className(),BaseMailer::EVENT_AFTER_SEND,function ($event) {$message = $event->message;$messageData = [// ...];$this->_messages[] = $messageData;});}// …public function save(){return $this->_messages;}}
同时,在我们自己的详情页中,它展示一个格子,里边是被存储消息的列表。
参考
- 为了了解更多关于
yii2-debug扩展,参考http://www.yiiframework.com/doc-2.0/ext-debug-index.html - 欲了解更多关于创建一个视图计数面板的信息,参考https://github.com/yiisoft/yii2-debug/blob/master/docs/guide/topics-creating-your-own-panels.md
