《应用Yii1.1和PHP5进行敏捷Web开发》

第十三章:迭代10:上线/投产准备

尽管我们的应用程序还有很多功能上的不足,(虚构)截止日期的临近和客户对产品投入生产环境感到很焦虑。但在投产之前还有一些事值得做。这正是我们最后一次迭代需要完成的工作。

迭代计划

我们将专注与以下任务,使得我们的应用程序适应生产环境:

  • 启动Yii的应用程序日志框架,使得所有致命的错误和事件都被记录下来
  • 启动Yii的错误控制框架,使得我们能明白它在开发与生产环境中的不同
  • 启动应用程序数据缓存来帮助增进性能

日志

日志是在应用程序开发的最后一步应该被提起的一个主题。信息,警告,严重错误信息在引起应用程序崩溃时时非常有价值的,在生产环境中它们大多被实际用户使用。

Yii提供了一个弹性且可扩展的日志功能。日志信息可以依据日志等级和信息类型被分类。通过使用等级和类型过滤器,使得被选中的信息被路由至不同目的地,比如写入磁盘文件,发送至管理员信箱,或者显示在浏览器窗口中。

日志信息

每次请求的时候我们的应用程序都会记录大量信息。当程序被初始化完成后,程序被配置成调试模式,并且在该模式下Yii框架自身记录日志信息。我们可以查看到该信息,因为默认的这些信息被保存在内存中。因此,它们的生存周期与请求相同。

根目录的index.php文件中的如下代码,决定了应用程序是否处于调试模式:

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

让我们在SiteController类添加一个小action来看看被记录的内容,代码如下:

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

如果我们通过请求:http://localhost/trackstar/site/showLog来调用该action,我们将看到与下面类似的画面:

如果我们注释掉在index.php中定义的全局应用程序条件变量,并刷新页面,我们将看不到日志内容。这是因为系统级调试信息等级事通过Yii::trace来创建的,只有当应用程序处于该特殊调试模式下才会记录信息。

我们可以通过以下2个静态方法中的一个来记录信息:

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

好像之前提到的,俩者的唯一区别就是Yii::trace方法只在调试模式下记录信息。

类型和等级

当记录一个信息,我们需要指定它的类型和等级。类型是表现为xxx.yyy.zzz格式的类似路径代理的字符串。例如,如果在SiteController类中记录一条信息,我们可以选择使用application.controllers.SiteController作为类型。类型为被记录信息提供了而外的内容。另外当使用Yii::log为被记录信息指定一个类型时可以同时指定一个等级。等级可以被认为是该消息的缩略。虽然你可以自定义等级,但是一般我们使用以下的一种:

  • Trace:这一等级一般被用来基于开发环境的应用程序工作流
  • Info:这个是日志的大概内容,而且也是未指定下的默认类型
  • Profile:这一等级被用来描述上面提到的性能方面的功能
  • Warning:警告信息
  • Error:错误信息

添加一个登录日志信息

作为一个例子,让我们为用户登录方法添加一些日志。我们将在该方法的开头添加一些基本调试信息,用来标识该方法正在被执行。然后在登录成功时我们将记录一个信息性的消息,同样的在登录失败时记录一个警告。按照如下代码修改我们的SiteController::actionLogin()方法:

/**
  * 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));
}

如果我们成功登录(或者进行了一次错误的尝试),然后访问我们的日志,我们看不到它们(如果你注释掉了调试模式申明,请确保你的应用程序已经开启调试模式)。再一次,原因是默认的,日志会存储在内存中。在请求完成后它们就会消失。这会显得很没价值。我们需要将其导航至持久存储的地方,这样我们才可以在请求之后查看我们生成的日志。

消息路由

如我们所说,默认的,Yii::log和Yii::trace将信息存储在内存中。一般的,将这些消息显示在浏览器窗口中会使得它们非常有价值,或者存储到一些持久化介质中,或者是发送email,甚至是数据库中。Yii的消息路由功能允许将日志信息路由至不同目的地。

在Yii中消息路由是由CLogRouter应用程序组件管理的。它允许你定义一系列消息目的地。

为了利用消息路由的功能,我们需要在protected/config/main.php中配置CLogRouter应用程序组件。我们通过设置它的routes属性为需要的路由地址来完成该动作。

如果我们打开配置文件,我们将看到一些预先设置好的配置信息(是在使用yiic的webapp命令生成应用程序时生成的)。下面是默认定义在应用程序中的内容:

'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',
        ), 
        */
    ),
),

路由应用程序组件被配置为使用框架的CLogRouter类。你可以使用自定义的类如果你的路由需求无法使用框架基本类来实现,但是在这里,不需要做改变。

在该类定义的下面定义了routes属性。在这个例子中,只定义了一个路由。它使用了Yii框架的消息路由类CFileLogRoute。CFileRoute消息路由类使用文件系统来保存消息。默认的,文件被保存至/protected/runtime/application.log中。事实上,如果你一直跟随着我们的教程并且完成了自己的应用程序,你可以打开该文件看看yii为你保存的消息。只有error和warning类型的消息将被路由至该文件。前面代码中注释掉了另外一个路由类CWebLogRoute。如果使用它,消息将会显示在当前页面。下面是一个Yii 1.1版的消息路由列表:

  • CDbLogRoute: 将消息存储到数据库表中
  • CEmailLogRoute:将消息发送至特定的e-mail地址
  • CFileLogRoute:将消息保存至应用程序runtime文件夹下的一个文件中
  • CWebLogRoute:在当前页面的结尾显示消息
  • CProfileLogRoute:在当前页面结尾显示profiling消息

添加日志至SiteController::actionLogin()方法中使用了Yii::trace和Yii::log。当使用Yii::trace时,日志的等级被自动设置为trace。使用Yii::log时,如果登录成功,我们将等级设置为info,如果失败则设置为warning级。让我们修改配置文件,将trace和info级消息写入一个叫infoMessages的新文件,该文件与application.log在同一文件夹。同时也配置其将warning消息显示在浏览器中。为了完成这一操作,我们对配置文件进行如下修改:

'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',
        ),
...

此时,在我们保存修改后,让我们看看改变。首先,进行一次成功登录。应用程序会因此写入2条消息到/protected/runtime/infoMessages.log文件(为新建文件),第一条为trace,后面一条为成功登录。成功登录后,文件内容如下(由于篇幅关系并未全部显示):

...
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
...

哇哦,这里的消息绝对不是2条。但是其中一定有我们写入的2条。它们在上面的列表中被加粗显示。现在开始,因为我们将trace路由至该新建文件,所以基于框架的所有trace都会出现在这里。这很好的帮助我们弄懂了应用程序中一条请求的生存周期。它比表面上看到的要多的多。在进入生产后,我们应该取消这一设置。在非调试模式,我们将只能看到info级的日志。不过这一等级的细节对追踪bug和弄清应用程序正在做什么非常重要。知道它在那里无论是否需要是让人很舒服的。

下面让我们看下登录失败的场景。现在让我们登出,并再次登录,但是这次使用错误的帐号密码使得登录失败,我们将在返回页面的底部看到warning级的消息,这证明我们的配置起作用了。下图显示了warning级消息显示的样子:

当我们使用CLogRouter来路由消息,日志文件被保存在logPath属性指定的目录下,并且文件名被指定为logFile。日志路由的另一个好处是自动轮换日志文件。如果日志文件的大小超过了maxFileSize(单位KB 千字节)属性中设置的值,轮换就被执行一次,当前日志文件被重命名为添加后缀1。所有存在的日志文件都会被默认向后更改文件名,例如.2到.3, .1到.2。maxLogFiles属性会被用来指定最大存在文件数量。

错误控制

合适的控制随机出现在软件程序中的错误是非常重要的。再次提示,这个主题应该出现在编写我们的应用程序之前,而不是当前的最后一阶段。幸运的是,因为我们使用了一些Yii自动生成的代码,我们的应用程序已经应用了一些Yii的差错控制功能。

Yii提供一个完整的基于PHP5 异常体系的错误控制框架,一个内置的机制通过关键点来控制程序错误。当主Yii应用程序组件被建立用来接收进入的用户请求,它注册了CApplication::handleError()方法来控制PHP的warnings和notices。它注册了CApplication::handleException()方法来控制未捕获的PHP异常。也就是说,在应用程序执行时一个PHP warning/notice或者一个未捕获的PHP异常发生了,上面一种错误控制将进行控制,并且执行所需的错误控制流程。

错误控制器的注册在应用程序的构造函数中,通过调用PHP的set_exception_handler和set_error_handler方法来实现。如果你不想让Yii来控制这种类型的错误和异常,你可以通过定义一个全局常量YII_ENABLE_ERROR和YII_ENABLE_EXCEPTION_HANDLER的形式来重写默认行为在主index.php脚本中未false。

默认的,应用程序将使用框架的CErrorHandler类作为一项应用程序组件任务来控制PHP错误和未捕获异常。这一应用程序组件的其中一部分任务就是使用适当的view文件来显示错误,不管应用程序处于调试模式或生产模式。这将允许你在不同环境下自定义错误信息。在开发模式下显示更多的(冗长的)错误信息是有必要的,可以帮助定位问题。但是允许用户在生产模式下观看该信息可能会引起安全问题。同时,如果你在你的网站里实行多语言化,CErrorHandler将选择最合适的语言来显示错误。

在Yii中抛出异常的方式与在PHP中抛出异常相同。使用下面的语法:

throw new ExceptionClass('ExceptionMessage');

Yii提供下面2个异常类:

  • CException
  • CHttpException

CException是一个一般的异常类。CHttpException描述了将会显示给最终用户的异常。CHttpException同时带有statusCode属性,来描述HTTP状态码。使用不同的异常类抛出的异常,显示错误方式不同。

显示错误

如前所述,当一个错误被提交至CErrorHandler应用程序组件,该组件会决定使用哪个视图文件来显示错误。如果是一个显示给最终用户的错误,类似使用CHttpException的,默认使用一个名称为errorXXX的视图文件,其中的xxx为HTTP响应的状态码(例如,400,404,500)。如果是一个内部事务并且只向开发人员显示,则使用exception视图文件。当应用程序处于调试模式,一个完整的堆栈调用和错误行在源文件中的位置将被显示。

遗憾的是,故事并未结束。当应用程序运行在生产模式,所有的错误将被使用errorXXX视图文件显示出来。这是因为包含堆栈调用的信息很敏感,不应该在向最终用户显示。

当应用程序处于生产模式,开发人员应当通过错误日志来了解更多的错误信息。级别为error的消息总是会被存储到日志中的。如果一个错误由PHP warning或 notice引起,消息将会同category php一起存储起来。如果错误由未捕获异常引起,类型将会是exception.ExceptionClassName,异常类名将会是CHttpException或CException或者它们子类中的一个。如前所述,使用日志的一个好处就是用来监控处于生产状态的应用程序中的错误。

默认的,CErrorHandler按照以下的顺序搜索相关view文件:

  1. WebRoot/themes/ThemeName/views/system:当前主题下的系统视图文件
  2. WebRoot/protected/views/system:某一应用程序的默认系统视图文件
  3. YiiRoot/framework/views:Yii框架提供的标准系统视图文件夹

所以你可以通过修改基于应用程序或主题下的系统view文件夹来自定义错误视图显示错误。

Yii也允许你定义特殊的控制器方法来控制错误的显示。也就是我们如何配置应用程序。我们将通过例子来观察。

让我们看一些相关的例子。下面一些通过Gii生成的代码包含了Yii的错误控制。其中的一个例子就是ProjectController::loadModel()方法。其代码如下:

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;
}

我们可以看到,以上代码通过输入的id值来尝试加载一个Project AR模型类的实例。如果无法定位请求的project类,则抛出一个CHttpException异常,让用户明白所请求的页面不存在。我们可以通过一个不存在的ID在我们的浏览器中对其进行测试。我们知道我们的ID99的project不存在,如果使用http://localhost/trackstar/project/view/id/99来进行请求,将会看到如下的画面:

这非常好,因为所有的页面都拥有相同的主题,页头,页脚,等等。这并非默认行为对错误页面的渲染。最初的应用程序被配置为使用一个特殊的控制器方法来控制相关错误。这就是我们提供的在应用程序中控制错误的另一种方法。如果我们观察一下配置文件,我们会看到下面的代码:

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

该配置表示应用程序组件使用SiteController::actionError()方法来控制所有准备向用户显示的异常。如果我们观察一下该方法,我们会发现它渲染了protected/views/site/error.php视图文件。这是一个普通的视图文件,所以它也会渲染相关的布局和主题文件。基于以上的原因,当错误发生的时候我们可以给用户提供一个友好的界面。

为了在不修改配置的情况下观察默认行为,让我们临时注销上面的配置代码(在 protected/config/main.php),然后再请求一次不存在的页面。我们将会看到如下反馈:

因为我们没有指定任何自定义错误页面,所以这是Yii框架自身的error404.php文件。

让我们继续,恢复对配置文件的修改,继续使用SiteController::actionError()方法控制错误。

让我们看一下CException与HTTP异常类抛出异常的区别。让我们注销HTTP相关代码,添加其他异常类,具体代码如下:

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;
}

现在如果我们请求一个不存在的project,我们看到的情况会大有不同。这次我们看到了一个包含全部堆栈跟踪跟踪错误信息的系统生成的错误页面同时也包含了出错的文件信息。这个画面有点太长不适合展示出来,但是一定会包含一个CException抛出的,描述信息为‘The is a example of throwing a CException’的源文件和堆栈信息。

因此在调试模式抛出不同的异常类,会有不同的结果。这是一类在开发环境中,为我们提供找到问题根源的信息。让我们在index.php中暂时注销调试模式设置,来看看在生产模式下的情况:

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

注销以后,如果通过刷新再次访问不存在的project,将会看到如下图的HTTP 500错误:

所以没有任何敏感信息会在生产模式暴露给用户。

缓存

缓存数据可以很好的帮助在生产状态下的web应用程序提高性能。如果这是一个不需要每次请求都发生改变的特殊内容,使用缓存来存储和提供这一内容,可以节约重新生成的时间。

在缓存方面Yii提供了一些不错的功能。Yii的缓存教程将从配置应用程序缓存组件开始。该组件是众多CCache子类中的一个,只是存储实现不同。

Yii提供了很多基于不同存储介质实现缓存的缓存组件类。下面是Yii 1.1.2所支持的缓存实现列表:

  • CMemCache : 使用PHP memcache 扩展。
  • CApcCache : 使用PHP APC扩展。
  • CXCache : 使用PHP XCache扩展。
  • CEAcceleratorCache : 使用PHP EAccelerator扩展。
  • CDbCache : 使用数据库表存储缓存数据。默认的,它将在runtime文件夹下建立一个SQLite3数据库。你可以通过connectionID属性指定数据库。
  • CZendDataCache : 使用Zend Data Cache作为优先的缓存媒介。
  • CFileCache : 使用文件来存储缓存数据。特别适合存储类似页面的大块数据。
  • CDummyCache : 这里实现一般的缓存接口,但不做任何实际缓存。这一实现的原因是你需要执行缓存,但是你的开发环境不支持缓存。这允许你在实现真正缓存之前持续编码。你不需要为缓存环境而改变编码。

所有的这些组件都扩展自CCache,并且提供相同的接口。也就是说你可以改变不同的缓存策略而不需要改变代码。

配置缓存

如前面所说,在Yii中使用缓存,需要选择一个缓存组件,然后在应用程序配置文件(/protected/config/main.php)中进行配置。不同的缓存实现决定了不同的配置。如果要使用memcached就要使用CMemCache类,是一个分布式内存对象缓存系统,允许你使用多台主机进行缓存,如果要使用2台服务器应该如下配置:

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), ),
            ),
    ),
);

为了使一切对于一直跟着TrackStar开发的读者相对简单,我们将使用基于CFileCache的文件系统缓存,我们将看一些例子。在所有的允许对文件系统进行读写操作的开发环境中都可以实现。

如果由于一些原因你无法这样做,但是你又想跟随本教程,请使用CDummyCache。和之前说的一样,它不会存储任何实质数据到缓存中,但是可以使代码正常运行。

CFileCache提供了一个基于文件系统的缓存结构。当使用它的时候,每一个数据值都会被缓存并存储在一个独立的文件中。默认的,这些文件被存储在/protected/runtime/cache文件夹下,但是可以通过修改cachePath属性来轻松改变这一位置。对我们来说默认已经ok了,所以我们只需要在配置文件/protected/config/main.php中进行如下配置:

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

当上面的都完成之后,我们可以在应用程序的任何位置通过Yii::app()->cache 来访问这一新组件。

使用基于文件的缓存

让我们试试这一新组件吧。还记得我们在上一次迭代中添加的基于管理功能的系统消息吗?比起每一次请求时都从数据库读取相关信息,让我们对该值进行缓存来节约时间,所以无需每一次请求都从数据库获取信息。

让我们在SysMessage AR模型类添加一个新公共方法来控制最后一次获取系统消息。让我们将该方法设置成public 和 static,这样无需实例化SysMessage就可以调用该方法。在测试中这也是非常有用的。

测试?你可能觉得我们放弃了测试先导的开发模式。实际上不是,让我们进行一下测试。

建立一个新的测试文件,protected/tests/unit/SysMessageTest.php,并且为其添加下面的部件定义和测试方法:

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

在命令行中运行该测试,会因为我们没有添加方法而立刻得到错误提示。让我们向SysMessage类添加如下方法:

/**
  * 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;
}

稍候我们就会谈论一下相关细节。首先让我们使测试通过。即使上面的改动都完成了,如果你再次运行测试,我们面对的仍然是错误。但是这次,错误的原因是我们的方法返回了null,我们的测试为一个非空返回值。返回null的原因是数据库中没有任何系统信息。请记住我们的测试都是基于trackstar_test数据库的。ok,没其他问题的话,使用夹具来解决它。在protected/tests/fixtures/tbl_sys_message.php添加内容如下的夹具文件:

<?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,
    ),
);

请确保用于测试的SysMessageTest类被配置使用夹具:

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

好,现在是时候再次运行测试了,成功是必然的。该方法将会尝试从缓存获取消息。但是因为这是第一次基于测试环境的请求,缓存里没有数据。所以将会从数据库中获取数据然后将结果存储到缓存中方便后续请求使用。

如果我们对默认缓存文件夹(/protected/runtime/cache/)做一个列表,会发现一个名字很奇怪的文件(可能会略有不同):

8b22da6eaf1bf772dae212cd28d2f4bc.bin

如果使用文本编辑器打开,会看到如下内容:

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;}

这是一个被序列化的关于我们最新从数据库中取回的SysMessage AR类实例的缓存值,正是我们希望在那的。这样说来,缓存正常工作。

当运行测试时,针对数据库在测试环境运行应用程序,我们可能会希望将缓存数据配置到不同的位置。如果是这样,你需要在我们测试配置文件, protected/config/test.php 添加一个略有不同的缓存组件。例如,如果我们需要指定一个不同的文件夹来存放缓存数据,我们需要向测试配置文件添加如下内容:

'cache'=>array( 
    'class'=>'system.caching.CFileCache', 
    'cachePath'=> '/Webroot/trackstar/protected/runtime/cache/test',
),
这样,当正常使用项目的时候不会对我们的测试数据造成影响。

让我们稍微仔细分析一下上面SysMessage::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;
}

正如我们所说的,我们配置缓存应用程序组件在应用程序中的任何位置都可以通过Yii::app()->cache来访问。所以,首先代码检查了该组件是否被定义。如果返回true,则通过$cache->get($key)方法来尝试从缓存获取数据。这或多或少是你所期盼的。其通过一个特殊的key来从缓存获取数据。该key是一个特殊的标识字段,用来指向缓存中存储的一部分数据。在我们的系统信息的例子里,我们只需要一次显示一条消息,因此针对一条消息显示的key会非常简单。key可以是任何字符串,因为它代表里缓存里我们需要的一块唯一数据。这里我们选择了具有描述性的字符串TrackStar.ProjectListing.SystemMessage作为key来存储和获取我们缓存的系统消息。

当这段代码第一次执行的时候,并没有任何与之相关的数据在缓存中。因此,调用包含这个key的$cache->get()将返回false。所以会继续执行下面的代码,使用AR类从数据库尝试取出适当的系统消息:

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

接下来执行下面的代码,首先检查是否从数据库获得任何数据。如果得到了,在返回数据前将其存入缓存,反之返回null。

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

如果一个正确的系统消息被返回,我们就使用$cache->set()方法将其存入缓存。该方法格式如下:

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

当将一段数据放入缓存时,必须设置一个唯一的key,用来存储数据。key就是上面提到的唯一的字符串值,value就是希望存入缓存的数据值。可以是任何格式的,唯一的要求是可以被序列化。duration参数是一个指定生存周期的可选项。确保缓存中的数据定时被刷新。默认值为0,也就是说数据永远不会过期,将永远存在缓存中。(实际上,在Yii中将小于等于0的持续时间理解为1年后过期。所以,并非永远,只是一个很长的时间。)

我们以下面的形式调用set()方法:

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

我们将key的值设置为我们之前定义的TrackStar.ProjectListing.SystemMessage,被存储的数据就是SystemMessage AR类返回的值,也就是表tbl_sys_message中的message栏,并且设置过期时间为300秒。因此数据会每5分钟从数据库读取一次来做更新。我们没有指定dependency。我们将在下面讨论这一可选参数。

缓存dependencies(依赖)

dependency参数允许以选择并且更复杂的形式来决定存储在缓存中的数据是否需要刷新。并非简单的定义一个过期时间,你的缓存策略可能为在特殊用户发出请求后缓存数据变为非法,或者是应用程序的状态,或者是文件系统中的文件最近刚被更新过。该参数允许你指定相关的缓存校验规则。

dependency是CCacheDependency或其子类的一个实例。Yii允许下面的缓存依赖:

  • CFileCacheDependency : 缓存数据将在特殊文件最后更新时间自上次检查后发生改变时变为非法。
  • CDirectoryCacheDependency : 类似上面的文件缓存依赖,不过该方法将检查指定文件夹下的所有文件和子文件夹。
  • CDbCacheDependency : 缓存中的数据将在自上次检查后相同语句在数据库中查询结果发生改变后变得非法。
  • CGlobalStateCacheDependency : 缓存中的数据将在特殊标识全局状态值发生改变后变得非法。全局状态是应用程序中的一个在多页面多请求中持久保存的一个变量。通过调用CApplication::setGlobalState()来设置。
  • CChainedCacheDependency : 该项允许你将多个缓存依赖链接起来。当链上的任一缓存依赖发生改变时,缓存中的值变为非法。
  • CexpressionDependency : 当特殊php表达式的结果发生改变时缓存中的数据变为非法。

为了提供一个混合的例子,让我们使用一个依赖,使得在表tbl_sys_message中的数据发生改变后缓存中的数据过期。不是武断的每5分钟过期一次,我们希望过期发生在真正需要的时候,也就是,在系统消息表中的某一条消息的update_time发生改变时。我们将使用CDbCacheDependency来实现它,因为它正是被设计为基于SQL语句返回结果来检验缓存合法的。

我们修改我们的set方法调用,设置持续时间为0,所以它不会基于时间过期,同时传入一个新的基于特定SQL语句的依赖实例:

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

将持续时间改为0并非使用依赖的先决条件。我们可以将持续时间保持为300。这只会增加一条缓存合法的验证规则。缓存中的数据会按照5分钟为最大值来完成校验,但是当小于5分钟的情况下,update_time发生了改变也会触发缓存更新。

当上面的都完成后,缓存只会在查询语句返回结果改变时进行更新。但这个例子有一点奇怪,因为我们最初使用缓存就是为了减少数据库查询次数。但是现在我们设置其在每次读取缓存数据前都查询一次数据库。但是如果缓存的数据足够复杂,这样一个简单的sql语句做缓存刷新判断就有一定价值了。使用什么样的缓存刷新规则,完全基于应用程序的需要。但是Yii确实提供了多种选择来帮助我们达到各种需求。

为了完成我们对应用程序使用缓存的计划,我们仍然需要对ProjectController::actionIndex()方法进行更新。非常简单,只需将从数据库读取系统消息更换为调用刚刚新建的方法,将ProjectController::actionIndex()方法中的:

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

更换为:

$sysMessage = SysMessage::getLatest();

现在在项目列表页显示的系统消息就是被文件缓存后的。

Fagment(片段)缓存

前面的例子示范了缓存的使用。简单的说就是从数据库中读取一段数据,然后存入缓存中。在Yii中还有其他可行的方法来存储有view脚本生成的页面的一部分,或者整个页面。

片段缓存用来缓存一个页面的一部分。我们可以在view脚本中使用片段缓存。我们通过使用CController::beginCache()和CController::endCache()方法来实现。这2个方法用来在渲染页面中标识内容中需要被缓存的部分。只是作为一个缓存练习的例子,我们需要一个唯一的key来标识被缓存的内容。一般来说,应该在view文件中按照如下的形式使用片段缓存:

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

如果调用beginCache()返回false,缓存中的内容将会被自动插入指定位置。反之,如果if语句返回true,endCache()之前的内容将被缓存。

声明片段缓存可选参数

当调用beginCache()方法时,我们提供一个数组作为第二个参数,该数组包含了自定义片段缓存的可选项。事实上,beginCache()和endCache()方法是一个方便的COutputCache过滤器/部件的包裹器。因此,这里的缓存选项可以为任意COutputCache的属性。

无可厚非的一个最常用属性就是持续时间,即指定了缓存中的内容保留的时间。它很像我们在之前系统消息缓存中使用的duration参数。你可以按照如下格式为beginCache()提供duration参数:

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

片段缓存的默认设置与数据缓存不同。如果我们不设置持续时间,默认为60秒,也就是60秒后更新。当使用片段缓存时有很多可选参数。具体请通过 API地址查看 COutputCache部分 http://www.yiiframework.com/doc/guide/caching.fragment

使用片段缓存

让我们将其运用到我们的TrackStar应用程序中。我们将再次关注项目列表页。你或许能记得,在页面的底部,为用户留下的针对相关project中issue的评论。这个列表指出了谁在某一个issue留下了评论。无需每次请求时都重新生成,让我们使用片段缓存来实现每2分钟刷新一次。应用程序会的数据会有一些陈旧,2分钟也并非等待评论列表更新所必须的时间。

为了做到这个,在view文件/protected/views/project/index.php进行修改。我们将使用片段缓存来缓存最近评论portlet:

<?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();
} 
?>

代码修改完后,当我们第一次访问项目列表页,将会在缓存中存入我们的评论列表。如果我们在2分钟内添加一个评论,然后返回项目列表页,我们将无法看到最新添加的评论。不过如果我们不断刷新页面,缓存过期后数据将被刷新,而我们将在列表中看到我们的新评论。

你可以简单的添加一个echo time(); PHP语句到被缓存的内容中,来看过期时间是否准确。在缓存数据被刷新前,显示的时间不会更新。当使用文件缓存时,请确保你的/protected/runtime/文件夹是可以被web服务器进程写入的,当然是在你使用默认缓存储存路径的情况下。

页面缓存

作为片段缓存的扩充,Yii提供了对整个页面进行缓存的选项。页面缓存和片段缓存很相似。然而,因为页面的内容经常是一个view文件调用布局文件的结果,所以我们无法简单的在布局文件中调用beginCache()和endCache()方法。原因是布局被应用到CController::render()方法中的view内容生成时。所以我们将错过从缓存获取数据的机会。

所以,想要缓存整个页面,我们需要跳过页面生成的过程。为了达到这一点,我们可以使用COutputCache类作为一个我们控制器类中的一个行为过滤器。

让我们拿出一个例子。让我们使用页面缓存的方法来缓存所有项目细节页。在TrackStar中,通过URL http://localhost/trackstar/project/view/id/[id]来请求项目细节页,其中[id]是所请求的项目id。我们想做的是设置一个页面缓存过滤器,按照ID将页面缓存起来。当缓存内容时我们需要将项目id混合到key的值中去。也就是说,我们不希望请求项目id为1的项目细节时缺得到项目2的缓存。幸运的是,COutputCache过滤器已经完成了这一工作。

打开 protected/controllers/ProjectController.php,并且将已经存在的filters()方法修改为如下形式:

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'),
        ),
    );
}

该filter配置为利用COutputCache过滤器来缓存整个被应用程序调用ProjectController::actionView()方法生成的内容。+ view 添加在 COutputCache后,如你所能记得的,是对特殊方法添加过滤器的标准的方式。持续时间被设置为页面生成后的2分钟。

varyByParam是我们之前提起过的一种很重要的可选项。为了给你减轻压力,减少编写标识被缓存内容key的工作量,该功能允许key被框架自动控制。也就是说,通过指定一系列来自GET参数中的名称。当我们开始通过project_id请求project时,系统将会很好的使用该id作为唯一key的一部分来生成缓存内容。通过指定'varyByParam'=>array('id'),COutputCache为我们完成了基于输入请求字符串中id的余下工作。这里有很多可选项,在我们使用COutputCache来缓存数据时,来作为自动生成key名字的策略。下面是可以使用的一个列表:

  • varyByRoute:通过将该选项设置为true,具体请求的路由部分将会作为独立标识符的一部分用于生成缓存数据。所以,你可以使用请求controller和acion的组合来区别缓存内容。
  • varyBySession:通过设置该选项为true,将使用唯一的session id来区分缓存中的内容。每个用户的session都是不同的,但是可以用来为缓存服务。
  • varyByParam:如前面所说,这里是用输入的GET中的参数来区分缓存内容。
  • varyByExpression:给该选项设置PHP表达式,我们可以使用相应表达式的结果来区分缓存的内容。

所以,当在ProjectController类中配置了上面的过滤器后,每次针对某个项目的请求内容都会被缓存,并且在2分钟后刷新缓存。你可以通过先缓存一个项目的细节,然后使用某种方法刷新缓存来查看。所有的更新都会在其生存周期结束后才可以观察到。

缓存全部页面内容是极大提升性能的一种方式,但是对每一个应用程序中的每一个页面进行缓存没有意义。做一个上面3种方式的总结:数据,片段和页面缓存,在大多数真实的项目中是需要的。我们只简单的了解了一下Yii中提供的缓存功能。希望这能为你迅速看清Yii的缓存功能提供助力。

性能调整技巧

在结束本次迭代之前,我们将简要的列出其他可以用来提升基于Yii开发web应用程序的领域。

这些内容或多或少引用自Performance Tuning部分,相关在线手册链接地址: http://www.yiiframework.com/doc/guide/topics.performance 但是在这里重提一下是很有意义的。

启用APC

启用PHP APC扩展可能是最简单的增进整个应用程序性能的方式。扩展的缓存和优化控制的PHP中间代码并且减少了每次请求时花费在PHP脚本分析上的时间。

禁用调试模式

在本章的早些时候我们提到过这一概念,在这里再提一次也不为过。禁用调试模式是另外一个提高安全和性能的方式。如果位于主index.php脚本中的YII_DEBUG常量被定义为true,Yii应用程序将运行在调试模式下。在调试模式下,许多包括在框架内的组件,将导致额外的损耗。

使用yiilite.php

当使用PHP APC扩展,一件可以做的事是使用Yii bootstrap(引导)文件yiilite.php代替yii.php。这可以很好的增加Yii应用程序的性能。yiilite.php文件包含在每一个yii发行版本中。它是一些常用Yii类文件合并的产物。因此使用yiilite.php可以减少文件的included量和trace语句。

使用yiilite.php,而不启用APC则会导致性能的下降,这是因为yiilite.php包含了一些并非每次请求都需要花费时间解析的类。另外一个显而易见的是因为服务器的配置也会引起yiilite.php使用后变慢,尽管该服务器已经开始APC。最好的鉴别是否适合使用yiilite.php的方法是针对hello world demo运行benchmark。

使用缓存技术

正如我们在本章提到的,Yii提供了一些可能会显著提升应用程序性能的缓存解决方案。下面列出了可以使用的缓存系统:

  • 当生成某些数据耗费了大量时间,我们使用数据缓存减少生成频率
  • 如果页面中的一部分相对静态,我们使用片段缓存来减少渲染频率
  • 如果整个页面相对静态,我们使用页面缓存来节约整个页面生存时间

启用schema缓存

如果应用程序使用了Active Record,可以在生产环境中启用schema缓存来减少分析数据库schema的时间。可以通过设置CDbConnection::schemaCachingDuration属性的值大于0来实现。

值得一提的是这些都是基于应用程序级的缓存技术,我们也可以使用服务器端缓存技术来提升应用程序的性能。上面提到的启用APC就是这个范畴的。还有一些其他的服务器端技术,例如:Zend Optimizer, eAccelerator, Squid,等。

绝大部分这些只是为了提供一个练习指导,来帮助你做好发布Yii应用程序的准备。大部分应用程序性能的调整需要更多的练习,并且很多都是在Yii框架之外的整体性能。起初Yii就将性能纳入首要任务,并且不断从其他基于PHP的应用程序中学习(通过http://www.yiiframework.com/performance/来了解更多内容)。当然,任何一个web应用程序都应当尽量提升性能,但是从一开始就选择Yii会使你的应用程序在性能上站在一个很高的高度。

小结

在本次最后的迭代中,我们关注了如何对处于生产环境的应用程序中进行维护和性能提升。首先我们学习了Yii的日志策略,同时也关注了如何按照不同的等级和类别记录和路由信息。然后我们关注了错误控制和Yii如何利用PHP5 抛出的异常来提供弹性控制。然后我们学习了Yii中提供的不同缓存策略。我们学习了如何缓存应用程序数据和不同粒度等级的内容。基于特定变量和独立数据库的的数据缓存,页面中一部分内容的片段缓存,和整个渲染结果页面内容的缓存。最后,我们列出一系列好的尝试,可以用来提升基于Yii的web应用程序性能。

不幸的,我们的TrackStar应用程序并未完成,全功能任务管理系统,以及很多留给读者来实现的概念。但是,一个好的基础已经打好,并且现在已经将Yii交入你的手中,你可以快速将这些变成功能丰富的应用程序。同时大量提到过的例子的身影会出现在其他你未来的应用程序中。祝你未来的项目开发愉快!

评论 X

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