《Agile Web Application Development with Yii1.1 and PHP5》

Chapter 13: Iteration 10: Production Readiness

Even though our application lacks a significant amount of feature functionality, our (albeit imaginary) deadlines are approaching and our (also imaginary) client is getting anxious about getting the application into a production environment. Although it may take some time before our application actually sees the light of day in production it is time to get the application "production ready". In this, our final development iteration, we are going to do just that.

Iteration planning

In order to achieve the goal of preparing our application for a production environment, we are going to focus on the following granular tasks:

  • Implement Yii's application logging framework to ensure we are logging information about critical production errors and events
  • Implement Yii's application error handling framework to ensure we understand how this works differently in a production environment than in a development environment
  • Implement application data caching to help improve performance

Logging

Logging is a topic that should arguably have been covered before this late stage in the application development. Informational, warning, and severe error messages are invaluable when it comes to troubleshooting software applications, most certainly those in a production environment being used by real users.

Yii provides a flexible and extensible logging feature. Messages logged can be classified according to log levels and message categories. Using level and category filters, selected messages can be further routed to different destinations, such as written to files on disc, sent to administrators as e-mails or displayed to browser windows.

Message logging

Our application has actually been logging many informational messages upon each request the entire time. When the initial application was created, it was configured to be in debug mode and, while in this mode, the Yii Framework itself logs information messages. We can't actually see these messages because, by default, they are being logged to memory. So, they are around only for the lifetime of the request.

Whether or not the application is in this debug mode is controlled by the following line in the root index.php file:

defined('YII_DEBUG') or define('YII_DEBUG',true);

To see what is being logged, let's whip up a quick little action method in our SiteController class to display the messages:

public function actionShowLog() 
{
    echo "Logged Messages:<br><br>"; 
    var_dump(Yii::getLogger()->getLogs());
}

If we invoke this action by making the following request at: http://localhost/ trackstar/site/showLog, we see something similar to the following:

If we comment out our global application debug variable, defined in index.php, and refresh the page, we'll notice that nothing was logged. This is because this system-level debugging information level logging is accomplished by calling Yii::trace, which only logs the message if the application is in this special debug mode.

We can log messages using one of two static application methods:

  • Yii::log($message, $level, $category)
  • Yii::trace($message, $category)

As mentioned, the main difference between these two methods is that Yii::trace logs the message only when the application is in debug mode.

Categories and levels

When logging a message, we need to specify its category and level. The category is represented by a string in the format of xxx.yyy.zzz, which resembles the path alias. For example, if a message is logged in our application's SiteController class, we may choose to use the category application.controllers.SiteController. The category is there to provide extra context to the message being logged. In addition to specifying the category, when using Yii::log, we can also specify a level for the message. The level can be thought of as the severity of the message. You can define your own levels, but typically they take on one of the following values:

  • Trace: This level is commonly used for tracing the execution flow of the application during development.
  • Info: This level is for logging general information, and it is the default level if none is specified.
  • Profile: This level is to be used with the performance profile feature, which is described below.
  • Warning: This level is for warning messages.
  • Error: This is level for fatal error messages.

Adding a login message log

As an example, let's add some logging to our user login method. We'll provide some basic debugging information at the beginning of the method to indicate the method is being executed. We'll then log an informational message upon a successful login as well as a warning message if the login fails. Alter our SiteController::actionLogin() method as follows:

/**
  * Displays the login page
  */ 
public function actionLogin() 
{
    Yii::app()->language = 'rev';
    Yii::trace("The actionLogin() method is being requested", 
        "application.controllers.SiteController");
 
    if(!Yii::app()->user->isGuest) 
    {
        $this->redirect(Yii::app()->homeUrl);
    } 
 
    $model=new LoginForm;
 
    // if it is ajax validation request 
    if(isset($_POST['ajax']) && $_POST['ajax']==='login-form') 
    {
        echo CActiveForm::validate($model); 
        Yii::app()->end();
    }
 
    // collect user input data 
    if(isset($_POST['LoginForm'])) 
    {
        $model->attributes=$_POST['LoginForm']; 
        // validate user input and redirect to the previous page if valid
        if($model->validate() && $model->login()) 
        {
            Yii::log("Successful login of user: " . Yii::app()->user- >id, 
                "info", "application.controllers.SiteController");
            $this->redirect(Yii::app()->user->returnUrl);
        }
        else {
            Yii::log("Failed login attempt", "warning", "application. controllers.SiteController");
        }
    }
 
    // display the login form 
    //public string findLocalizedFile(string $srcFile, string $srcLanguage=NULL, string $language=NULL)
    $this->render('login',array('model'=>$model));
}

If we now successfully log in (or perform a failed attempt) and visit our page to view the logs, we don't see them (If you commented out the debug mode declaration, make sure you have put the application back in debug mode for this exercise). Again, the reason is that by default, the logging implementation in Yii simply stores the messages in memory. They disappear when the request completes. This is not terribly useful. We need to route them to a more persistent storage area so we can view them outside of the request in which they are generated.

Message routing

As we mentioned, by default, messages logged using Yii::log or Yii::trace are kept in memory. Typically, these messages are more useful if they are displayed in browser windows, or saved to some persistent storage such as in a file, or in a database or sent as an e-mail. Yii's message routing allows for the log messages to be routed to different destinations.

In Yii, message routing is managed by a CLogRouter application component. It allows you to define a list of destinations to which the log messages should be routed.

In order to take advantage of this message routing, we need to configure the CLogRouter application component in our protected/config/main.php config file. We do this by setting its routes property with the desired log message destinations.

If we open our config file, we see that some configuration information has already been provided (again, courtesy of using the yiic webapp command to initially create our application). The following is already defined in our configuration:

'log'=>array(
    'class'=>'CLogRouter', 
    'routes'=>array(
        array( 
            'class'=>'CFileLogRoute', 
            'levels'=>'error, warning',
        ), 
        // uncomment the following to show log messages on web pages 
        /* 
        array(
            'class'=>'CWebLogRoute',
        ), 
        */
    ),
),

The log application component is configured to use the framework class CLogRouter. You could also certainly create and use a custom child class of this if you have logging requirements not fully met by the base framework implementation, but in our case, this will work just fine.

What follows the class definition in the previous configuration is the definition of the routes property. In this case, there is just one route specified. This one is using the Yii Framework message routing class, CFileLogRoute. The CFileLogRoute message routing class uses the filesystem to save the messages. By default, messages are logged in a file under the application runtime folder, /protected/runtime/ application.log. In fact, if you have been following along with us and have your own application, you can take a peek at this file and will see several messages that have been logged by the framework. The levels specification dictates that only messages whose log level is either error or warning will be routed to this file. The part of the configuration in the preceding code that is commented out specifies another route, CWebLogRoute. If used, this will route the message to be displayed on the currently requested web page. The following is a list of message routes currently available in version 1.1 of Yii:

  • CDbLogRoute: Saves messages in a database table
  • CEmailLogRoute: Sends messages to specified e-mail addresses
  • CFileLogRoute: Saves messages in a file under the application runtime folder
  • CWebLogRoute: Displays messages at the end of the current web page
  • CProfileLogRoute: Displays profiling messages at the end of the current web page

The logging that we added to our SiteController::actionLogin() method used Yii::trace for one message and then used Yii::log for two more. When using Yii::trace, the log level is automatically set to trace. When using the Yii::log we specified an info log level if the login was successful and a warning level if the login attempt failed. Let's alter our log routing configuration to write the trace and info level messages to a new, separate file called infoMessages.log in the same folder as our application.log file. Also, let's configure it to write the warning messages to the browser. To do that, we make the following changes to the configuration:

'log'=>array( 
    'class'=>'CLogRouter', 
    'routes'=>array(
        array( 
            'class'=>'CFileLogRoute',
            'levels'=>'error',
        ), 
        array(
            'class'=>'CFileLogRoute', 
            'levels'=>'info, trace', 
            'logFile'=>'infoMessages.log',
        ), 
        array(
            'class'=>'CWebLogRoute', 
            'levels'=>'warning',
        ),
...

Now, after saving these changes, let's try out the different scenarios. First, try a successful login. Doing so will write two messages out to our new /protected/ runtime/infoMessages.log file, one for the trace and then one logging the successful login. After successfully logging in, viewing that file reveals the following (The full listing was truncated to save a few trees):

...
2010/04/15 00:31:52 [trace] [application.controllers.SiteController] The actionLogin() method is being requested 
2010/04/15 00:31:52 [trace] [system.web.CModule] Loading "user" application component
2010/04/15 00:31:52 [trace] [system.web.CModule] Loading "session" application component 2010/04/15 00:31:52 [trace] [system.web.CModule] Loading "db" application component
2010/04/15 00:31:52 [trace] [system.db.CDbConnection] Opening DB connection 
...
2010/04/15 00:31:52 [info] [application.controllers.SiteController] Successful login of user: 1
...

Wow, there is a lot more in there than just our two messages. But our two did show up; they are bolded in the above listing. Now that we are routing all of trace messages to this new file, all of the framework trace messages are showing up here as well. This is actually very informative and helps you get a picture of the lifecycle of a request as it makes its way through the framework. There is a lot going on under the covers. We would obviously turn off this verbose level of logging when moving this application to production. In non-debug mode, we would only see our single info level message. But this level of detail can be very informative when trying to track down bugs and just figure out what the application is doing. It is comforting to know it is here when/if ever needed.

Now let's try the failed login attempt scenario. If we now log out and try our login again, but this time specify incorrect credentials to force a failed login, we see our warning level display along the bottom of the returned web page, just as we configured it to do. The following screenshot shows this warning being displayed:

When using the CLogRouter message router, the logfiles are stored under the logPath property and the filename is specified by the logFile. Another great feature of this log router is automatic logfile rotation. If the size of the logfile is greater than the value set in the maxFileSize (in kilobytes) property, a rotation is performed, which renames the current logfile by suffixing the filename with '1'. All existing logfiles are moved backwards one place, that is, '.2' to '.3', '.1' to '.2'. The property maxLogFiles can be used to specify how many files are to be kept.

Handling errors

Properly handling the errors that invariably occur in software applications is of the utmost importance. This, again, is a topic that arguably should have been covered prior to coding our application, rather than at this late stage. Luckily, though, as we have been leaning on tools within the Yii Framework to autogenerate much of our core application skeleton, our application is already taking advantage of some of Yii's error handling features.

Yii provides a complete error handling framework based on PHP 5 exceptions, a built-in mechanism for handling program failures through centralized points. When the main Yii application component is created to handle an incoming user request, it registers its CApplication::handleError() method to handle PHP warnings and notices. It registers its CApplication::handleException() method to handle uncaught PHP exceptions. Consequently, if a PHP warning/notice or an uncaught exception occurs during the application execution, one of the error handlers will take over the control and start the necessary error handling procedure.

The registration of error handlers is done in the application's constructor by calling the PHP functions set_exception_handler and set_ error_handler. If you prefer to not have Yii handle these types of errors and exceptions, you may override this default behavior by defining a global constant YII_ENABLE_ERROR_HANDLER and YII_ENABLE_ EXCEPTION_HANDLER to be false in the main index.php entry script.

By default, the application will use the framework class CErrorHandler as the application component tasked with handling PHP errors and uncaught exceptions. Part of the task of this built-in application component is displaying these errors using appropriate view files based on whether or not the application is running in debug mode or in production mode. This allows you to customize your error messages for these different environments. It makes sense to display much more verbose error information in a development environment, to help troubleshoot problems. But allowing users of a production application to view this same information could compromise security. Also, if you have implemented your site in multiple languages, CErrorHandler also chooses the most preferred language for displaying the error.

You raise exceptions in Yii the same way you would normally raise a PHP exception. One uses the following general syntax to raise an exception when needed:

throw new ExceptionClass('ExceptionMessage');

The two exception classes the Yii provides are:

  • CException
  • CHttpException

CException is a generic exception class. CHttpException represents an exception that is intended to be displayed to the end user. CHttpException also carries a statusCode property to represent an HTTP status code. Errors are displayed differently in the browser, depending on the exception class that is thrown.

Displaying errors

As was previously mentioned, when an error is forwarded to the CErrorHandler application component, it makes a decision as to which view file to use when displaying the error. If the error is meant to be displayed to end users, such as is the case when using CHttpException, the default behavior is to use a view named errorXXX, where XXX represents the HTTP status code (for example, 400, 404, 500). If the error is an internal one and should only be displayed to developers, it will use a view named exception. When the application is in debug mode, a complete call stack as well as the error line in the source file will be displayed.

However, this is not the full story. When the application is running in production mode, all errors will be displayed using the errorXXX view files. This is because the call stack of an error may contain sensitive information that should not be displayed to just any end user.

When the application is in production mode, developers should rely on the error logs to provide more information about an error. A message of level error will always be logged when an error occurs. If the error is caused by a PHP warning or notice, the message will be logged with category php. If the error is caused by an uncaught exception, the category will be exception.ExceptionClassName, where the exception class name is one of, or child class of, either CHttpException or CException. One can thus take advantage of the logging features, discussed in the previous section, to monitor errors that occur within a production application.

By default, CErrorHandler searches for the location of the corresponding view file in the following order:

  1. WebRoot/themes/ThemeName/views/system: The system view file under the currently active theme.
  2. WebRoot/protected/views/system: The default system view file for an application.
  3. YiiRoot/framework/views: The standard system view folder provided by the Yii Framework.

So, you can customize the error display by creating custom error view files under the system view folder of the application or theme.

Yii also allows you to define a specific controller action method to handle the display of the error. This is actually how our application is configured. We'll see this as we go through a couple of examples.

Let's look at a couple examples of this in action. Some of the code that was generated for us as a by-product of using the Gii CRUD generator tool to create our CRUD scaffolding is taking advantage of Yii's error handling. One such example is the ProjectController::loadModel() method. That method is defined as follows:

public function loadModel() 
{
    if($this->_model===null) 
    {
        if(isset($_GET['id'])) 
            $this->_model=Project::model()->findbyPk($_GET['id']);
        if($this->_model===null)
            throw new CHttpException(404,'The requested page does not exist.');
    } 
    return $this->_model;
}

We see that it is attempting to load the appropriate Project model AR instance based on the input id querystring parameter. If it is unable to locate the requested project, it throws a CHttpException as a way to let the user know that the page they are requesting, in this case the project details page, does not exist. We can test this in our browser by explicitly requesting a project that we know does not exist. As we know our application does not have a project associated with an ID of 99, a request for http://localhost/trackstar/project/view/id/99 will result in the following page being returned:

This is nice, because the page looks like any other page in our application, with the same theme, header, footer, and so on. This is actually not the default behavior for rendering this type of error page. Our initial application was configured to use a specific controller action for the handling of such errors. We mentioned this was another option for how to handle errors in an application. If we take a peek into this configuration file, we see the following code snippet:

'errorHandler'=>array( 
    // use 'site/error' action to display errors 
    'errorAction'=>'site/error',
),

This configures our error handler application component to use the SiteController::actionError() method to handle all of the exceptions intended to be displayed to users. If we take a look at that action method, we notice that it is rendering the protected/views/site/error.php view file. This is just a normal controller view file, so it will also render any relevant application layout files and will apply the appropriate theme. This way, we are able to provide the user with a very friendly experience when certain errors happen.

To see what the default behavior is, without this added configuration, let's temporarily comment out the above lines of configuration code (in protected/ config/main.php) and request the non-existent project again. Now we see the following page:

As we have not explicitly defined any custom error pages following the convention outlined earlier, this is the error404.php file in the Yii Framework itself.

Go ahead and revert these changes to the configuration file to have the error handling use the SiteController::actionError() method.

Now let's see how this compares to throwing a CException, rather than the HTTP exception class. Let's comment out the current line of code throwing the HTTP exception and add a new line to throw this other exception class, as follows:

public function loadModel() 
{
    if($this->_model===null) 
    {
        if(isset($_GET['id'])) 
            $this->_model=Project::model()->findbyPk($_GET['id']);
        if($this->_model===null)
            //throw new CHttpException(404,'The requested page does not exist.');
            throw new CException('The is an example of throwing a CException');
    } 
    return $this->_model;
}

Now if we make our request for a non-existent project, we see a very different result. This time we see a system generated error page with a full stack trace error info dump along with the specific source file where the error occurred. The results were a little too long to capture in a screenshot, but it will display the fact that a CException was thrownalongwiththedescriptionThe is an example of throwing a CException, the source file and then the full stack trace.

So throwing this different exception class, along with the fact the application is in debug mode, has a different result. This is the type of information we would like to display to help us troubleshoot the problem, but only as long as our application is running in a secure development environment. Let's temporarily comment out the debug setting in the root index.php file, in order to see what would be displayed when in production mode:

// remove the following line when in production mode 
//defined('YII_DEBUG') or define('YII_DEBUG',true);

With this commented out, if we refresh our request for our non-existent project, we see that the exception is displayed as an end-user friendly HTTP 500 error, as depicted in the following screenshot:

So we see that none of our sensitive code or stack trace information is displayed when in production mode.

Caching

Caching data is a great method for helping to improve the performance of a production web application. If there is specific content that is not expected to change upon every request, using the cache to store and serve this content can save the time it takes to retrieve and process that data.

Yii provides for some nice features when it comes to caching. The tour of Yii's caching features will begin with configuring a cache application component. Such a component is one of several child classes extending CCache, the base class for cache classes with different cache storage implementations.

Yii provides many different specific cache component class implementations that store the data utilizing different approaches. The following is a list of the current cache implementations that Yii provides as of version 1.1.2:

  • CMemCache: Uses the PHP memcache extension.
  • CApcCache: Uses the PHP APC extension.
  • CXCache: Uses PHP XCache extension
  • CEAcceleratorCache: Uses the PHP EAccelerator extension.
  • CDbCache: Uses a database table to store cached data. By default, it will create and use a SQLite3 database under the runtime folder. You can explicitly specify a database for it to use by setting its connectionID property.
  • CZendDataCache: Uses Zend Data Cache as the underlying caching medium.
  • CFileCache: Uses files to store cached data. This is particular suitable to cache large chunk of data (such as pages).
  • CDummyCache: This presents the consistent cache interface, but does not actually perform any caching. The reason for this implementation is to that if you are faced with situation where your development environment does not have cache support, you can still execute and test your code that will need to use cache once available. This allows you to continue to code to a consistent interface, and when the time comes to actually implement a real caching component. You will not need to change the code written to write to or retrieve data from cache.

All of these components extend from the same base class, CCache and expose a consistent API. This means that you can change the implementation of the application component in order to use a different caching strategy without having to change any of the code that is using the cache.

Configuring for cache

As was mentioned, using cache in Yii typically involves choosing one of these implementations, and then configuring the application component for use in the / protected/config/main.php file. The specifics of the configuration will, of course, depend on the specific cache implementation. For example, if one were to use the memcached implementation, that is, CMemCache, which is a distributed memory object caching system that allows you to specify multiple host servers as your cache servers, configuring it to use two servers might look similar to:

array( 
    ......
    'components'=>array( 
        ......
        'cache'=>array( 
            'class'=>'system.caching.CMemCache', 
            'servers'=>array(
                array('host'=>'server1', 'port'=>12345, 'weight'=>60),
                array('host'=>'server2', 'port'=>12345,'weight'=>40), ),
            ),
    ),
);

To keep things relatively simple for the reader following along with the TrackStar development, we'll use the filesystem implementation, CFileCache, as we go through some examples. This should be readily available on any development environment that allows access to reading and writing files from the filesystem.

If for some reason this is not an option for you, but you still want to follow along with the code examples, simply use the CDummyCache option. As mentioned, it won't actually store any data in the cache, but the code will execute against it just fine.

CFileCache provides a file-based caching mechanism. When using this implementation, each data value being cached is stored in a separate file. By default, these files are stored under the protected/runtime/cache/ folder, but one can easily change this by setting the cachePath property when configuring the component. For our purposes, this default is fine, so we simply need to add the following to the components array in our /protected/config/main.php configuration file as such:

// application components 
'components'=>array(
    ...
    'cache'=>array( 
        'class'=>'system.caching.CFileCache',
    ),
    ...
),

With this in place, we can access this new application component anywhere in our running application via Yii::app()->cache.

Using a file-based cache

Let's try out this new component. Remember that system message we added as part of our administrative functionality in the previous iteration? Rather than get it from the database upon every request, let's store the value initially returned from the database in our cache for a limited amount of time, so that not every subsequent request has to retrieve the data from the database.

Let's add a new public method to our SysMessage AR model class to handle the retrieval of the latest system messages. Let's make this new method both public and static so that other parts of the application can easily use this method to access the latest system message without having to explicitly create an instance of SysMessage. This also will help in writing our test.

Test? You'd probably thought we forgot all about our test-first approach to development at this point. Well, we haven't, so let's get back to it.

Create a new test file, protected/tests/unit/SysMessgeTest.php, and add to it the following a fixture definition and single test method:

<?php 
class SysMessageTest extends CDbTestCase 
{
    public function testGetLatest() 
    {
        $message = SysMessage::getLatest(); 
        $this->assertTrue($message instanceof SysMessage);
    }
}

Running this test from the command line will immediately fail due to the fact that we have not yet added this new method. Let's add this method to the SysMessage class as follows:

/**
  * Retrieves the most recent system message.
  * @return SysMessage the AR instance representing the latest system message.
  */
public static function getLatest() 
{
    //see if it is in the cache, if so, just return it 
    if( ($cache=Yii::app()->cache)!==null) 
    {
        $key='TrackStar.ProjectListing.SystemMessage'; 
        if(($sysMessage=$cache->get($key))!==false)
            return $sysMessage;
    }
 
    //The system message was either not found in the cache, or 
    //there is no cache component defined for the application 
    //retrieve the system message    from the database
    $sysMessage = SysMessage::model()->find(array( 
        'order'=>'t.update_time DESC',
    ));
 
    if($sysMessage != null)
    {
        //a valid message was found. Store it in cache for future retrievals
        if(isset($key)) 
            $cache->set($key,$sysMessage,300);
        return $sysMessage;
    } 
    else
        return null;
}

We'll cover the details in just a minute. First, let's get our test to pass. With this in place, if we run our test again, we still get a failure. But this time, the failure is because our method is returning null, and we are testing for a non-null return value. The reason that it is returning null is that there are no system messages in our test database. Remember, our tests are run against the trackstar_test database. Okay, no problem, fixtures to the rescue. Add a new fixture file protected/tests/fixtures/tbl_sys_ message.php which is similar to look this:

<?php 
return array(
    'message1'=>array( 
        'message' => 'This is a test message', 
        'create_time' => new CDbExpression('NOW()'), 
        'create_user_id' => 1, 
        'update_time' => new CDbExpression('NOW()'), 
        'update_user_id' => 1,
    ),
);

Also, ensure that the test case class is configured to be using the fixture by verifying the following code is at the top of the SysMessageTest test class:

public $fixtures=array( 
    'messages'=>'SysMessage',
);

Okay, now we can fire off our test again, and this time it succeeds. The method should have tried to retrieve the message from the cache. But, as this was the first time for the request in our test environment, it would not yet be there. So, it proceeded to retrieve it from the database and then store the result into cache for subsequent requests.

If we do a folder listing for the default location being used for file caching, protected/runtime/cache/, we do indeed see one strangely named file (yours may be slightly different):

8b22da6eaf1bf772dae212cd28d2f4bc.bin

Which if we open in a text editor, reveals the following:

a:2:{i:0;O:10:"SysMessage":11:{s:18:"CActiveRecord_md";N;s:19:"18 CActiveRecord_new";b:0;
s:26:"CActiveRecord_attributes";a:6:{s:2 :"id";s:1:"1";s:7:"message";s:22:"This is a test message";
s:11:"create_time";s:19:"2010-07-08 21:42:00";s:14:"create_user_id";s:1:"1";s:11:"update_time";
s:19:"2010- 07-08 21:42:00";s:14:"update_user_id";s:1:"1";}s:23:"18CActiveRecord18_related";a:0:{}s:17:"CActiveRecord_c";
N;s:18:"CActiveRecord_pk";s :1:"1";s:15:"CModel_errors";a:0:{}s:19:"CModel_validators";
N; s:17:"CModel_scenario";s:6:"update";s:14:"CComponent_e";N;s:1 4:"CComponent_m";N;}i:1;N;}

This is the serialized, cached value of our most recently updated SysMessage AR class instance, which is exactly what we would expect to be there. So, we see that the caching is actually working.

When running tests, executing the application in the test environment, against the test database, we might want to configure a different location to cache our test data. In this case, we might want to add to our test application configuration, protected/config/test.php, a cache component that is configured slightly differently. For example, if we wanted to specify a different folder to place the test cache data, we could add the following to our application components in this test config file:

'cache'=>array( 
    'class'=>'system.caching.CFileCache', 
    'cachePath'=> '/Webroot/trackstar/protected/runtime/cache/test',
),
This way, we won't alter our test results by reading that was cached from normal use of the main development application.

Let's revisit the above code for our new SysMessage::getLatest() method in a bit more detail. The first thing the code is doing is checking to see if the requested data is already in the cache, and if so, returns that value:

//see if it is in the cache, if so, just return it 
if( ($cache=Yii::app()->cache)!==null) 
{
    $key='TrackStar.ProjectListing.SystemMessage'; 
    if(($sysMessage=$cache->get($key))!==false)
        return $sysMessage;
}

As we mentioned, we configured the cache application component to be available anywhere in the application via Yii::app()->cache. So, it first checks to see if there even is such a component defined. If so, it attempts to look up the data in the cache via the $cache->get($key) method. This does more or less what you would expect. It attempts to retrieve a value from cache based on the specified key. The key is a unique string identifier that is used to map to each piece of data stored in the cache. In our system message example, we only need to display one message at a time, and therefore can have a fairly simple key identify the single system message to display. The key can be any string value, as long as it remains unique for each piece of data we want to cache. In this case we have chosen the descriptive string TrackStar. ProjectListing.SystemMessage as the key used when storing and retrieving our cached system message.

When this code is executed for the very first time, there will not yet be any data associated with this key value in the cache. Therefore, a call to $cache->get() for this key will return false. So, our method will continue to the next bit of code, which simply attempts to retrieve the appropriate system message from the database, using the AR class:

$sysMessage = SysMessage::model()->find(array( 
    'order'=>'t.update_time DESC',
));

We then proceed with the following code that first checks if we did get anything back from the database. If we did, then it stores it in the cache before returning the value, otherwise, null is returned:

if($sysMessage != null) 
{
    if(isset($key)) 
        $cache->set($key,$sysMessage->message,300);
    return $sysMessage->message; 
} 
else
    return null;

If a valid system message was returned, we use the $cache->set() method to store the data into cache. This method has the following general form:

set($key,$value,$duration=0,$dependency=null)

When placing a piece of data into cache, one must specify a unique key, as well as the data to be stored. The key is a unique string value, as discussed above, and the value is whatever data desired to be cached. This can be in any format, as long as it can be serialized. The duration parameter specifies an optional time-to-live (TTL) requirement. This can be used to ensure that the cached value is refreshed after a period of time. The default is 0, which means it will never expire, that is, it will live forever in the cache. (Actually, internally, Yii translates a value of <=0 for the duration to mean that it should expire in one year. So, not exactly forever, but definitely a long time).

We are calling the set() method in the following manner:

$cache->set($key,$sysMessage->message,300);

We set the key to be what we had it defined as before, TrackStar.ProjectListing. SystemMessage, the data being stored is the message attribute of our returned SystemMessage AR class, that is, the message column of our tbl_sys_message table, and then we set the duration to be 300 seconds. This way, the data in the cache will expire every five minutes, at which time the database is queried again for the most recent system message. We did not specify a dependency when we set the data. We'll discuss this optional parameter next.

Cache dependencies

The dependency parameter allows for an alternative and much more sophisticated approach to deciding whether or not the stored data in the cache should be refreshed. Rather than declaring a simple time period for the expiration of cached data, your caching strategy may require that the data become invalid based on things like the specific user making the request, or the general mode or state of the application, or whether a file on the filesystem has been recently updated. This parameter allows you to specify such cache validation rules.

The dependency is an instance of CCacheDependency or its child class. Yii makes available the following specific cache dependencies:

  • CFileCacheDependency: The data in the cache will be invalid if the specified file's last modification time has changed since the previous cache lookup.
  • CDirectoryCacheDependency: Similar to the above for the file cache dependency, but this checks all the files and subdirectories within a given specified folder.
  • CDbCacheDependency: The data in the cache will be invalid if the query result of a specified SQL statement is changed since the previous cache lookup.
  • CGlobalStateCacheDependency: The data in the cache will be invalid if the value of the specified global state is changed. A global state is a variable that is persistent across multiple requests and multiple sessions in an application. It is defined via CApplication::setGlobalState().
  • CChainedCacheDependency: This allows you to chain together multiple dependencies. The data in the cache will become invalid if any of the dependencies on the chain is changed.
  • CExpressionDependency: The data in the cache will be invalid if the result of the specified PHP expression is changed.

To provide a concrete example, let's use a dependency to expire the data in the cache whenever a change to the tbl_sys_message database table is made. Rather than arbitrarily expire our cached system message after five minutes, we'll expire it exactly when we need to, that is, when there has been a change to the update_time column for one of the system messages in the table. We'll use the CDbCacheDependency implementation to achieve this, since it is designed to invalidate cached data based on a change in the results of a SQL query.

We alter our call to the set() method to set the duration time to 0, so that it won't expire based on time, but pass in a new dependency instance with our specified SQL statement as such:

$cache->set($key, $sysMessage->message, 0, 
      new CDbCacheDependency('select id from tbl_sys_message order by update_ time desc'));

Changing the duration TTL time to 0 is not at all a prerequisite of using a dependency. We could have just as easily left the duration in as 300 seconds. This would just stipulate another rule to render the data in the cache invalid. The data would only be valid in the cache for a maximum of five minutes, but would also be regenerated prior to this time limit if there as a change to the update_time column occurred on one or more records in the table.

With this in place, the cache will expire only when the results of the query statement are changed. This example is a little contrived, since we were originally caching the data to avoid a database call altogether. Now we have configured it to execute a database query every time we attempt to retrieve data from cache. However, if the cached data was a much more complex data set, that involved much more overhead to retrieve and process, a simple SQL statement for cache validity could make a lot of sense. The specific caching implementation, the data stored, the expiration time as well as any other data validation in the form of these dependencies will all depend on the specific requirements of the application being built. It is good to know that Yii has many options available to help meet our varied requirements.

To complete the changes to our application to take advantage of the caching of data in our new method, we still need to refactor the ProjectController::actionIn dex() method to use this newly create method. This is easy. Just replace the code that was generating the system message from the database, with a call to this new method. That is, in ProjectController::actionIndex(), simply change this:

$sysMessage = SysMessage::model()->find(array('order'=>'t.update_time DESC',));

to the following:

$sysMessage = SysMessage::getLatest();

Now the system message being displayed on the projects listing page is taking advantage of the file cache.

Fragment caching

The previous example demonstrates the use of data caching. This is where we take a single piece of data and store it in the cache. There are other approaches available in Yii to store fragments of pages generated by a portion of a view script, or even the entire page itself.

Fragment caching refers to caching a fragment of a page. We can take advantage of fragment caching inside of view scripts. To do so, we use the CController::beginCache() and CController::endCache() methods. These two methods are used to mark the beginning and the end of the rendered page content that should be stored in cache. Just as is the case when using a data caching approach, we need a unique key to identify the content being cached. In general, the syntax for using fragment caching inside of a view script is as follows:

...some HTML content... 
<?php if($this->beginCache($key)) { ?> 
...content to be cached... 
<?php $this->endCache(); } ?> 
...other HTML content...

If the call to beginCache() returns false, the cached content will be automatically inserted at that place; otherwise, the content inside the if statement will be executed and will be cached when endCache() is invoked.

Declaring fragment caching options

When calling beginCache(), we can supply an array as the second parameter consisting of caching options to customize the fragment caching. As a matter of fact, the beginCache() and endCache() methods are a convenient wrapper of the COutputCache filter/widget. Therefore, the caching options can be initial values for any properties of COutputCache.

Arguably one of the most common options specified when caching data is the duration, which specifies how long the content can remain valid in the cache. It is similar to the duration parameter we used when using the data caching approach for our system messages. You can specify the duration parameter when calling beginCache() as follows:

$this->beginCache($key, array('duration'=>3600))

The default setting for this fragment caching approach is different than that for the data caching. If we do not set the duration, it defaults to 60 seconds, meaning the cached content will be invalidated after 60 seconds. There a many other options you can set when using the fragment caching. For more information, refer to the API documentation for COutputCache as well as the fragment caching section of the definitive guide, available on the Yii Framework site: http://www.yiiframework.com/doc/guide/caching.fragment

Using fragment cache

Let's implement this in our TrackStar application. We'll again focus on the project listings page. As you recall, towards the bottom of this page, there is a list of the comments that users have left on the issues associated with each project. This list indicates who left a comment on which issue. Rather than re-generate this list upon each request, let's use fragment caching to cache this list for, say, two minutes. The application can tolerate this data being slightly stale, and two minutes is really not that long to have to wait for an updated comment list.

To do this, we make our changes to the listing view file, protected/views/ project/index.php. We'll wrap the call to our entire recent comments portlet inside this fragment caching approach, as such:

<?php 
$key = "TrackStar.ProjectListing.RecentComments"; 
if($this->beginCache($key, array('duration'=>120))) {
    $this->beginWidget('zii.widgets.CPortlet', array( 
        'title'=>'Recent Comments',
    )); 
    $this->widget('RecentComments'); 
    $this->endWidget(); 
    $this->endCache();
} 
?>

With this in place, if we visit the project listings page for the first time, our comments list will be stored in the cache. If we then quickly (by quickly, we mean before two minutes have elapsed) add a new comment to one of the issues within a project, and then toggle back to the project listings page, we won't immediately see the newly added comment. But if we keep refreshing the page, once the content in the cache expires (a maximum of two min in this case), the data will be refreshed, and our new comment will be displayed in the listing.

You could also simply add an echo time(); PHP statement to the above cached content to see if it is working as expected. If the content is properly caching, the time display will not update until the cache is refreshed. When using the file cache, remember to ensure that your /protected/ runtime/ folder is writable by the web server process, as this is where the cache content is stored by default.

Page caching

In addition to fragment caching, Yii offers options to cache the results of the entire page request. Page caching is similar to that of fragment caching. However, because the content of an entire page is often generated by applying additional layouts to a view, we can't simply call beginCache() and endCache() in the layout file. The reason is because the layout is applied within the call to the CController::render() method after the content view is evaluated. So, we would always miss the opportunity to retrieve the content from the cache.

Therefore, to cache a whole page, we should entirely skip the execution of the action generating the page content. To accomplish this, we can use COutputCache class as an action filter in our controller class.

Let's provide an example. Let's use the page caching approach to cache the page results for every project detail page. The project detail pages in TrackStar are rendered by requesting URLs of the format, http://localhost/trackstar/ project/view/id/[id], where [id] is the specific project ID we are requesting the details of. What we want to do is set up a page caching filter that will cache the entire contents of this page, separately for every ID requested. We need to incorporate the project ID into the key value when we cache the content. That is, we don't want to make a request for the details of project #1, and have the application return a cached result for project #2. Luckily, the COutputCache filter anticipated this need.

Open protected/controllers/ProjectController.php and alter the existing filters() method as such:

public function filters() 
{
    return array( 'accessControl', // perform access control for CRUD operations
        array(
            //cache the entire output from the actionView() method for 2 minutes
            'COutputCache + view', 
            'duration'=>120, 
            'varyByParam'=>array('id'),
        ),
    );
}

This filter configuration utilizes the COutputCache filter to cache the entire output generated by the application from a call to ProjectController::actionView(). The + view added just after the COutputCache declaration, as you may recall, is the standard way we include specific action methods to which a filter should apply. The duration parameter specifies a TTL of 120 seconds (2 min), after which the page content will be regenerated.

The varyByParam configuration is a really great option that we alluded to before. Rather than putting the responsibility on you, the developer, to come up with a unique key strategy for the content being cached, this feature allows the variation to be handled automatically. In this case, by specifying a list of names that correspond to GET parameters in the input request. Since we are caching the page content of requests for projects by project_id, it makes perfect sense to use this id as part of the unique key generation for caching the content. By specifying 'varyByParam'=>array('id'), COutputCache does this for us, based on the input querystring parameter, id. There are more options available to achieve this type of auto content variation strategy when using COutputCache to cache our data. As of this writing, the following variation features are available to use:

  • varyByRoute: By setting this option to true, the specific request route will be incorporated into the unique identifier for the cached data. Therefore, you can use the combination of the requested controller and action to distinguish cached content.
  • varyBySession: By setting this option to true, the unique session id is used distinguish the content in the cache. Each user session may see different content but all of this content can still be served from the cache.
  • varyByParam: As discussed previously, this uses the input GET querystring parameters to distinguish the content in the cache.
  • varyByExpression: By setting this option to a PHP expression, we can use the result of this expression to distinguish the content in the cache.

So, with the above filter configured in our ProjectController class, each request for a specific project details page is stored in the cache for up to two minutes before being regenerated and again stored in the cache. You can test this out by first viewing a specific project, then updating that project in some way. Your updates will not immediately display if done within the TTL of two minutes.

Caching entire page results is a great way to improve site performance, however it certainly does not make sense for every page in every application. A combination of the above three approaches: data, fragment and page caching, will probably need to be implemented in most real-world applications. We have really just scratched the surface of all caching options available within Yii. Hopefully this has whet your appetite to further investigate the full caching landscape available.

General performance tuning tips

Before we wrap up this final iteration, we'll briefly outline some other areas of consideration when working to tweak the performance of a Yii-based web application.

These more or less come straight from the Performance Tuning section of the Yii definitive guide, http://www.yiiframework.com/doc/guide/topics.performance. But it is good to restate them here for completeness and general awareness.

Using APC

Enabling the PHP APC extension is perhaps the easiest way to improve the overall performance of an application. The extension caches and optimizes PHP intermediate code and avoids the time spent in parsing PHP scripts for every incoming request.

Disabling debug mode

We discussed this earlier in the chapter, but it won't hurt to hear it again. Disabling debug mode is another easy way to improve performance and security. A Yii application runs in debug mode if the constant YII_DEBUG is defined as true in the main index.php entry script. Many components, including those down in the framework itself, incur extra overhead when running in debug mode.

Using yiilite.php

When the PHP APC extension is enabled, one can replace yii.php with a different Yii bootstrap file named yiilite.php. This can help to further boost the performance of a Yii-powered application. The file yiilite.php comes with every Yii release. It is the result of merging some commonly used Yii class files. Both comments and trace statements are stripped from the merged file. Therefore, using yiilite.php would reduce the number of files being included and avoid execution of trace statements.

Using yiilite.php without APC may actually reduce performance, because yiilite.php contains some classes that are not necessarily used in every request and would take extra parsing time. It is also observed that using yiilite.php is slower with some server configurations, even when APC is turned on. The best way to judge whether to use yiilite.php or not is to run a benchmark using the included hello world demo.

Using caching techniques

As we described and demonstrated in this chapter, Yii provides many caching solutions that may improve the performance of a web application significantly. The available caching systems are as follows:

  • If the generation of some data takes long time, we can use the data caching approach to reduce the data generation frequency
  • If a portion of page remains relatively static, we can use the fragment caching approach to reduce its rendering frequency
  • If a whole page remains relative static, we can use the page caching approach to save the rendering cost for the whole page

Enabling schema caching

If the application is using Active Record, one can turn on the schema caching in a production environment to save the time of parsing database schema. This can be done by configuring the CDbConnection::schemaCachingDuration property to be a value greater than 0.

Besides these application-level caching techniques, we can also use server-side caching solutions to boost the application performance. The enabling of APC caching that we described above belongs to this category. There are other server-side techniques, such as Zend Optimizer, eAccelerator and Squid, just to name a few.

These, for the most part, just provide some good-practice guidelines as you work to prepare your Yii application for production, or as you troubleshoot an existing application for bottlenecks. General application performance tuning is much more art than science, and there are many, many factors outside of the Yii Framework that play into the overall performance. Yii has been built with performance in mind since its inception and continues to out-perform many other PHP-based application development frameworks by a long shot (see http://www.yiiframework.com/ performance/ for more details). Of course, every single web application will need to be tweaked to enhance its performance, but making Yii the development framework of choice certainly puts your application on a great performance footing from the onset.

Summary

In this final iteration, we turned our attention to making changes to our application to help improve its maintainability and performance in a production environment. We first covered application logging strategies available in Yii, and how to log and route messages based on varying severity levels and categories. We then turned focus to error handling and how Yii exploits the underlying exception implementation in PHP 5 to provide a flexible and robust error handling framework. We then learned about some different caching strategies available in Yii. We learned about the caching of application data and content at varying levels of granularity. Data caching for specific variables or individual pieces of data, fragment caching for content areas within pages, and full page caching to cache the entire rendered output of a page request. Finally, we provided a list of "good practices" to follow when working to improve the performance of a Yii-powered Web application.

Unfortunately, our TrackStar application is actually quite far from a complete, full featured task management system, and even many of the concepts covered were left to the reader to fully implement. However, a nice foundation on which to build has been laid, and now that you have the power of Yii on your side, you could very quickly turn this into a much more useable and feature-rich application. Also a great many of the examples covered will translate well to other types of Web applications you may be building. Good luck with your future projects, and happy developing!

评论 X

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