《Yii 1.1应用程序开发实例》

第九章:错误处理,调试与日志

本章中,我们将涵括以下内容:

  • Using different log routes
  • Analyzing the Yii error stack trace
  • Logging and using the context information
  • Implementing your own smart 404 handler

介绍

It is not possible to create a bug-free application if it is relatively complex, so developers have to detect errors and deal with them as fast as possible. Yii has a good set of utility classes to handle logging and handling errors. Moreover, in the debug mode, Yii gives you a stack trace if there is an error. Using it, you can fix errors faster.

In this chapter, we will review logging, analyzing the exception stack trace, and implementing our own error handler.

Using different log routes

Logging is the key to understanding what your application actually does when you have no chance to debug it. Believe or not, but even if you are 100% sure that the application will behave as expected, in production it can do many things you were not aware of. This is OK, as no one can be aware of everything. Therefore, if we are expecting an unusual behavior, we need to know about it as soon as possible and have enough details to reproduce it. This is where logging comes in handy.

Yii allows a developer not only to log messages but also to handle them differently depending on the message level and category. You can, for example, write a message to DB, send an e-mail, or just show it in the browser.

In this recipe, we will handle log messages in a wise manner: The most important message will be sent through an e-mail, less important messages will be saved in files A and B, and the profiling will be routed to Firebug. Additionally, in a development mode, all messages and profiling information will be displayed on the screen.

准备工作

Set up a fresh Yii application by using yiic webapp as described in the official guide.

怎么做...

进行以下步骤:

  1. Configure logging using protected/config/main.php:
array( ...
    'preload'=>array('log'),
    'components'=>array(
        ...
        'log'=>array(
            'class'=>'CLogRouter',
            'routes'=>array(
                array(
                    'class' => 'CEmailLogRoute',
                    'categories' => 'example',
                    'levels' => CLogger::LEVEL_ERROR,
                    'emails' => array('admin@example.com'),
                    'sentFrom' => 'log@example.com',
                    'subject' => 'Error at example.com',
                ),
                array(
                   'class' => 'CFileLogRoute',
                   'levels' => CLogger::LEVEL_WARNING,
                   'logFile' => 'A',
                ), 
                array(
                    'class' => 'CFileLogRoute',
                    'levels' => CLogger::LEVEL_INFO,
                    'logFile' => 'B',
                ), 
                array(
                    'class' => 'CWebLogRoute',
                    'categories' => 'example',
                    'levels' => CLogger::LEVEL_PROFILE,
                    'showInFireBug' => true,
                    'ignoreAjaxInFireBug' => true,
                ), 
                array(
                    'class' => 'CWebLogRoute',
                    'categories' => 'example',
                ),
            ),
        ),
    ), 
),
  1. Now, we will produce a few log messages in protected/controllers/ LogController.php as follows:
<?php
class LogController extends CController
{
    public function actionIndex()
    {
        Yii::trace('example trace message', 'example');
        Yii::log('info', CLogger::LEVEL_INFO, 'example');
        Yii::log('error', CLogger::LEVEL_ERROR, 'example');
        Yii::log('trace', CLogger::LEVEL_TRACE, 'example');
        Yii::log('warning', CLogger::LEVEL_WARNING, 'example');
        Yii::beginProfile('preg_replace', 'example');
        for($i=0;$i<10000;$i++){
            preg_replace('~^[ a-z]+~', '', 'test it');
        }
        Yii::endProfile('preg_replace', 'example');
 
        echo 'done';
    }
}
  1. Now run the preceding action multiple times. On the screen, you should see a web log similar to the one shown in the following screenshot:

A web log contains all the messages we have logged along with stack traces, timestamps, levels, and categories.

  1. Now open Firefox's Firebug console. You should see profiler messages as shown in the following screenshot:

Yii uses firebug-compatible API, so you can view these messages in Chrome as follows:

Alternatively, in Opera:

  1. As we just changed the log file names and not paths, you should look in protected/ runtime/ to find log files named A and B. Inside, you will find messages as follows:
2011/04/17 00:58:53 [warning] [example] warning
in W:\home\yiicmf\www\protected\controllers\LogController.php (11)
in W:\home\yiicmf\www\index.php (12)
2011/04/17 00:59:00 [warning] [example] warning
in W:\home\yiicmf\www\protected\controllers\LogController.php (11)
in W:\home\yiicmf\www\index.php (12)
2011/04/17 00:59:56 [warning] [example] warning
in W:\home\yiicmf\www\protected\controllers\LogController.php (11)
in W:\home\yiicmf\www\index.php (12)
2011/04/17 01:02:07 [warning] [example] warning
in W:\home\yiicmf\www\protected\controllers\LogController.php (11)
in W:\home\yiicmf\www\index.php (12)
2011/04/17 01:02:12 [warning] [example] warning
in W:\home\yiicmf\www\protected\controllers\LogController.php (11)
in W:\home\yiicmf\www\index.php (12)
2011/04/17 01:03:00 [warning] [example] warning
in W:\home\yiicmf\www\protected\controllers\LogController.php (11)
in W:\home\yiicmf\www\index.php (12)

它是如何工作的...

When one logs a message using Yii::log or Yii::trace, Yii passes it to the log router. Depending on how it is configured, it passes messages to one or many routes. For example, e-mailing errors, writing debug information in file A, writing warning information in file B, and passing profiling results to the Firebug console. This is depicted in the following diagram:

CLogRouter is typically attached to an application component named log. Therefore, in order to configure it, we should set its properties in the protected/config/main.php, components section. The only configurable property there is routes that contains an array of log route handlers and their configurations.

We have defined five log routes. Let's review them as follows:

array(
    'class' => 'CEmailLogRoute',
    'categories' => 'example',
    'levels' => CLogger::LEVEL_ERROR,
    'emails' => array('admin@example.com'),
    'sentFrom' => 'log@example.com',
    'subject' => 'Error at example.com',
),

CEmailLogRoute sends log messages through an e-mail. We limit category to example and level to error. An e-mail will be sent from log@example.com to admin@example.com and the subject will be Error at example.com.

array(
    'class' => 'CFileLogRoute',
    'levels' => CLogger::LEVEL_WARNING,
    'logFile' => 'A',
),

CFileLogRoute appends error messages to a file specified. We limit the message level to warning and use a file named A. We do the same for info level messages by using a file named B.

CWebLogRoute passes log messages to the browser as follows:

array(
    'class' => 'CWebLogRoute',
    'categories' => 'example',
),

The same log route can be used to pass log messages to Firebug or another console compatible with it.

array(
    'class' => 'CWebLogRoute',
    'categories' => 'example',
    'levels' => CLogger::LEVEL_PROFILE,
    'showInFireBug' => true,
    'ignoreAjaxInFireBug' => true,
),

In the preceding code, we limit category to example, log level to profile, and turn on the logging to Firebug. Additionally, we turn the logging off for AJAX-requests, as the JSON response can be spoiled by logging.

还有更多...

There are more interesting things about Yii logging, which are covered in the following subsection:

Yii::trace vs Yii::log

Yii::trace is a simple wrapper around Yii::log:

if(YII_DEBUG)
    self::log($msg,CLogger::LEVEL_TRACE,$category);

Therefore, Yii::trace logs a message with a trace level, if Yii is in the debug mode.

Yii::beginProfile and Yii::endProfile

These methods are used to measure the execution time of some part of the application's code. In our LogController, we measured 10,000 executions of preg_replace as follows:

Yii::beginProfile('preg_replace', 'regex_test');
for($i=0;$i<10000;$i++){
    preg_replace('~^[ a-z]+~', '', 'test it');
}
Yii::endProfile('preg_replace', 'regex_test');
Log messages immediately

By default Yii keeps all log messages in memory until the application is properly terminated using Yii::app()->end(). That's done for performance reasons and generally works fine. However, if there is a PHP fatal error or die()/exit() in the code, some log messages will not be written at all. To make sure your messages will be logged you can flush them explicitly using Yii::app()->log->flush(true).

进一步阅读

In order to learn more about logging, refer to the following URLs:

  • http://www.yiiframework.com/doc/guide/en/topics.logging
  • http://www.yiiframework.com/doc/api/CLogRouter
  • http://www.yiiframework.com/doc/api/CLogRoute
  • http://www.yiiframework.com/doc/api/CDbLogRoute
  • http://www.yiiframework.com/doc/api/CEmailLogRoute
  • http://www.yiiframework.com/doc/api/CFileLogRoute
  • http://www.yiiframework.com/doc/api/CWebLogRoute
  • http://www.yiiframework.com/doc/api/CLogger

另请参阅

  • The recipe named Logging and using the context information in this chapter

Analyzing the Yii error stack trace

When an error occurs, Yii can display the error stack trace along with the error. A stack trace is especially helpful when we need to know what really caused an error rather than just the fact that an error occurred.

准备工作

  • Set up a fresh Yii application by using yiic webapp as described in the official guide
  • Configure a database and import the following SQL:
CREATE TABLE `article` (
    `alias` varchar(255) NOT NULL,
    `title` varchar(255) NOT NULL,
    `text` text NOT NULL,
    PRIMARY KEY (`alias`)
));
  • Generate an Article model using Gii

怎么做...

进行以下步骤:

  1. Now we will need to create some code to work with. Create protected/ controllers/ErrorController.php as follows:
<?php
class ErrorController extends CController
{
    public function actionIndex()
    {
        $articles = $this->getModels('php');
        foreach($articles as $article)
        {
            echo $article->title;
            echo "<br />";
        }
    }
    
    private function getModels($alias)
    {
        $criteria = new CDbCriteria();
        $criteria->addSearchCondition('allas', $alias);
        return Article::model()->findAll($criteria);
    }
}
  1. We will run the preceding action and we should get the following error:
CDbCommand failed to execute the SQL statement: SQLSTATE[42S22]:
Column not found: 1054 Unknown column 'allas' in 'where clause'.
The SQL statement executed was: SELECT * FROM `article` `t` WHERE
allas LIKE :ycp0
  1. Moreover, the stack trace shows the following error:

它是如何工作的...

From the error message, we know that we have no allas column in DB, but used it somewhere in the code. In our case, it is very simple to find it just by searching all the project files, but in a large project, a column can be stored in a variable. Moreover, we have everything to fix an error without leaving a screen where the stack trace is displayed. We just need to read it carefully.

The stack trace displays a chain of calls in the reversed order starting with the one that caused an error. In our case, it is queryInternal. Generally, we don't need to read the whole trace to get what is going on. The framework code itself is tested well, so the probability of error is less. That is why Yii displays the application trace entries expanded and the framework trace entries collapsed.

Therefore, we take the first expanded section and look for allas. After finding it, we can immediately tell it is used in ErrorController.php at line 17.

还有更多...

In order to learn more about error handling, refer to the following URL:

http://www.yiiframework.com/doc/guide/en/topics.error

另请参阅

  • The recipe named Logging and using the context information in this chapter

Logging and using the context information

Sometimes a log message is not enough to fix an error. For example, if you are following best practices and developing and testing an application with all possible errors reported, you can get an error message. However, without the execution context, it is only telling you that there was an error and it is not clear what actually caused it.

For our example, we will use a very simple and poorly coded action that just echoes "Hello, <username>!" where the username is taken directly from $_GET.

准备工作

  • Configure PHP to use the most strict error reporting. In php.ini, replace the error_ reporting value with "-1". This change requires restarting the server.
  • Set up a fresh Yii application by using yiic webapp as described in the official guide.

怎么做...

进行以下步骤:

  1. First, we will need a controller to work with. Therefore, create protected/controllers/ LogController.php as follows:
<?php
class LogController extends CController
{
    public function actionIndex()
    {
        echo "Hello, ".$_GET['username'];
    }
}
  1. Now, if we run the index action, we will get the error message "Undefined index: username". Let's configure the logger to write these kind of errors to a file. protected/config/main.php:
array( 
    ...
    'preload'=>array('log'),
    'components'=>array(
        ...
        'log'=>array(
            'class'=>'CLogRouter',
            'routes'=>array(
                array(
                    'class'=>'CFileLogRoute',
                    'levels'=>'error',
                    'logFile' => 'errors',
                ),
                ...other log routes...
            ),
        ), 
    ),
)
  1. Run the index action again and check protected/runtime/error. There should be log information like the following:
2011/04/17 03:53:19 [error] [php] Undefined index: username (W:\
home\yiicmf\www\protected\controllers\LogController.php:30)
Stack trace:
#0 W:\home\yiicmf\framework\web\CController.php(280):
LogController->runAction()
#1 W:\home\yiicmf\framework\web\CController.php(258):
LogController->runActionWithFilters()
#2 W:\home\yiicmf\framework\web\CWebApplication.php(329):
LogController->run()
#3 W:\home\yiicmf\framework\web\CWebApplication.php(122):
CWebApplication->runController()
#4 W:\home\yiicmf\framework\base\CApplication.php(155):
CWebApplication->processRequest()
#5 W:\home\yiicmf\www\index.php(12): CWebApplication->run()
REQUEST_URI=/log/environment
in W:\home\yiicmf\www\protected\controllers\LogController.php (30)
in W:\home\yiicmf\www\index.php (12)
  1. Now we can give our application to a testing team and check the errors log from time to time. We will know there are errors, but we will need to reproduce them somehow. In order to do this, we need to reconstruct the environment. Let's add it to the log as follows:
array(
    'class' => 'CFileLogRoute',
    'levels' => CLogger::LEVEL_ERROR,
    'logFile' => 'errors',
    'filter'=>'CLogFilter',
),
  1. Now run action again. This time, you should get enough to reproduce the environment in detail:
2011/04/17 04:01:16 [info] [application] $_SERVER=array (
    'REDIRECT_STATUS' => '200',
    'HTTP_USER_AGENT' => 'Opera/9.80 (Windows NT 6.1; U; ru)
        Presto/2.8.131 Version/11.10',
    'HTTP_HOST' => 'yiicmf',
    'HTTP_ACCEPT' => 'text/html, application/xml;q=0.9, application/
        xhtml+xml, image/png, image/webp, image/jpeg, image/gif,
        image/x-xbitmap, */*;q=0.1',
    'HTTP_ACCEPT_LANGUAGE' => 'ru-RU,ru;q=0.9,en;q=0.8',
    'HTTP_ACCEPT_ENCODING' => 'gzip, deflate',
    'HTTP_PRAGMA' => 'no-cache',
    'HTTP_CACHE_CONTROL' => 'no-cache',
    'HTTP_CONNECTION' => 'Keep-Alive',
    
...
 
    'SERVER_NAME' => 'yiicmf',
    'SERVER_ADDR' => '127.0.0.1',
    'SERVER_PORT' => '80',
    'REMOTE_ADDR' => '127.0.0.1',
    'DOCUMENT_ROOT' => 'W:/home/yiicmf/www',
    'SERVER_ADMIN' => 'admin@localhost',
    'SCRIPT_FILENAME' => 'W:/home/yiicmf/www/index.php',
    'REMOTE_PORT' => '55190',
    'REDIRECT_URL' => '/log/environment',
    'GATEWAY_INTERFACE' => 'CGI/1.1',
    'SERVER_PROTOCOL' => 'HTTP/1.1',
    'REQUEST_METHOD' => 'GET',
    'QUERY_STRING' => '',
    'REQUEST_URI' => '/log/environment',
    'SCRIPT_NAME' => '/index.php',
    'PHP_SELF' => '/index.php',
    'REQUEST_TIME' => 1302998476,
    'argv' =>
        array (
        ),
    'argc' => 0,
    )
2011/04/17 04:01:16 [error] [php] Undefined index: username (W:\
home\yiicmf\www\protected\controllers\LogController.php:30)
Stack trace:
#0 W:\home\yiicmf\framework\web\CController.php(280):
LogController->runAction()
#1 W:\home\yiicmf\framework\web\CController.php(258):
LogController->runActionWithFilters()
#2 W:\home\yiicmf\framework\web\CWebApplication.php(329):
LogController->run()
#3 W:\home\yiicmf\framework\web\CWebApplication.php(122):
CWebApplication->runController()
#4 W:\home\yiicmf\framework\base\CApplication.php(155):
CWebApplication->processRequest()
#5 W:\home\yiicmf\www\index.php(12): CWebApplication->run()
REQUEST_URI=/log/environment
in W:\home\yiicmf\www\protected\controllers\LogController.php (30)
in W:\home\yiicmf\www\index.php (12)
2011/04/17 04:01:16 [info] [application] User: Guest (ID: )

它是如何工作的...

In order to get more from the error logging, we use the CLogFilter class that preprocesses the logged messages before they are handled by a log route. It is the only log filter that is included in the Yii core and can be used to add more information about the execution context and environment. If we are logging a message manually, then we probably know what information we need, so we can set some CLogFilter options to write only what we really need:

array(
    'class' => 'CFileLogRoute',
    'levels' => CLogger::LEVEL_ERROR,
    'logFile' => 'errors',
    'filter'=> array(
        'class' => 'CLogFilter',
        'logUser' => false,
        'logVars' => array('_GET'),
    ),
),

The preceding code will log errors to a file named errors. Additionally to a message itself, it will log contents of $_GET if it is not empty.

还有更多...

In order to learn more about log filters and context information, refer to the following URLs:

  • http://www.yiiframework.com/doc/api/CLogFilter
  • http://www.yiiframework.com/doc/guide/en/topics.logging#logging-context-informations

另请参阅

  • The recipe named Using different log routes in this chapter

Implementing your own smart 404 handler

In Yii, the error handling is very flexible, so you can create your own error handler for errors of specific type. In this recipe, we will handle a 404 not found error in a smart way: We will show a custom 404 page that will suggest the content based on what was entered in the address bar.

准备工作

  • Set up a fresh Yii application by using yiic webapp as described in the official guide
  • Configure a database and import the following SQL:
CREATE TABLE `article` (
    `alias` varchar(255) NOT NULL,
    `title` varchar(255) NOT NULL,
    `text` text NOT NULL,
    PRIMARY KEY (`alias`)
));
  • Generate an Article model using Gii
  • Generate an Article crud using Gii
  • Create the following articles:
TitleAliasText
Yii frameworkyii-frameworkYii framework is good.
Why Yiiwhy-yiiWhy should I use Yii?
PHP is a good toolphp-is-a-good-toolPHP is nice!
Yii as a tool for appsyii-as-a-tool-for-appsYii is a good tool for apps.

怎么做...

进行以下步骤:

  1. Try to run http://your.application/yii. You should get a standard 404-error page. Now we need to somehow change this page, but leave it as it is for other error types. In order to achieve this, we will use the application's onException event. Let's configure it to be handled by the handle static method of the NotFoundHandler class. We will do it by using protected/config/main.php as follows:
// events
'onException' => array('NotFoundHandler', 'handle'),
  1. Now we need to implement the error handling itself. Create protected/ components/NotFoundHandler.php as follows:
<?php
class NotFoundHandler
{
    public static function handle(CExceptionEvent $event)
    {
        $exception = $event->exception;
        if(get_class($exception)=="CHttpException" &&
            $exception->statusCode===404)
        {
            $pathParts = explode('/', Yii::app()->getRequest()->getRequestUri());
            $pathPart = array_pop($pathParts);
            $criteria = new CDbCriteria();
            $criteria->addSearchCondition('alias', $pathPart);
            $criteria->limit = 5;
            $models = Article::model()->findAll($criteria);
            $controller = new CController(null);
            $controller->renderPartial('//error/404', array(
                'models' => $models,
            ));
            $event->handled = true;
        }
    }
}
  1. Also, we will need a view named protected/views/error/404.php:
<!doctype html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>404</title>
    </head>
    <body>
        <h1>404</h1>
        
        <?php if(!empty($models)):?>
        <p>Probably you've searched for:</p>
        <ul>
            <?php foreach($models as $model):?>
            <li><a href="<?php echo $this->createUrl('article/view',
                array('id' => $model->alias))?>"><?php echo
                $model->title?></a></li>
            <?php endforeach?>
        </ul>
        <?php endif?>
    </body>
</html>
  1. That is it. Now try the following URLs:
  • http://your.application/yii
  • http://your.application/tool

Each time, you will get several links to related articles.

它是如何工作的...

By using the configuration file, we attach an event handler to the onException event as follows:

'onException' => array('NotFoundHandler', 'handle'),

This means that we will use NotFoundHandler::handle(). Every event handler method accepts a single parameter named $event with event data inside. The exception handler parameter accepts the CExceptionEvent instance. As it contains the original exception, we can check for its type and error code as follows:

if(get_class($exception)=="CHttpException" &&
    $exception->statusCode===404)

If the exception doesn't match, the Yii works as before and if it does match, Yii executes our custom code:

$pathParts = explode('/', Yii::app()->getRequest()->getRequestUri());
$pathPart = array_pop($pathParts);

We get the last URL segment as it will most probably contain the article alias if the URL is in path format. Then, form DB criteria and get models as follows:

$criteria = new CDbCriteria();
$criteria->addSearchCondition('alias', $pathPart);
$criteria->limit = 5;
$models = Article::model()->findAll($criteria);

Then, we render a view in the following way:

$controller = new CController(null);
$controller->renderPartial('//error/404', array(
    'models' => $models,
));

Note that we are creating a new CController instance because 404 can be reached before the application will initialize a controller.

$event->handled = true;

Finally, we instruct Yii that the event is handled and there is no need to handle it further.

还有更多...

The preceding method is not the only method for handling errors in a customized way. Other options are as follows:

  • Extend CErrorHandler application component
  • Use a controller action to handle errors by setting CErrorHandler:: errorAction
进一步阅读

In order to learn more about handling errors in Yii, refer to the following API pages:

  • http://www.yiiframework.com/doc/api/CErrorHandler/
  • http://www.yiiframework.com/doc/api/CApplication#onException-detail
  • http://www.yiiframework.com/doc/api/CApplication#onError-detail

另请参阅

  • The recipe named Using Yii events in Chapter 1, Under the Hood
评论 X

      友荐云推荐
      Copyright 2011-2014. YiiBook.com