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

第一章:深入底层

深入底层

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

  • 使用getters和setters
  • 使用Yii事件(处理机制)
  • 使用导入和自加载
  • 使用异常(处理机制)
  • 配置组件
  • 配置挂件默认属性
  • 使用Yii核心集合组件
  • 处理请求

介绍

本章中我们将涉及很多不为人知的有趣的Yii特性。这些大部分只是在Yii框架的api里面有所描述,但是因为他们在指南(http://www.yiiframework.com/doc/guide/1.1/zh_cn/index没被提及,即便有也是很简洁的带过,所以只有很有经验的Yii开发者才经常用这些。然而,这里描述的Yii特性相对简单,正是这些特性使得Yii开发有趣又富有成效。

使用getters和setters

Yii有许多来自其他语言的特性,像Java和C#。其中之一就是,用getters和setters为从CComponent继承来的类定义属性(事实上真是任何Yii的类)。

这条秘诀将教会你怎样去用getters和setters来定义自己的属性,怎样使你的属性只读,怎样在原本PHP的分配中掩藏自定义过程。

怎么做…

  1. 因为PHP没有语言级的属性,所以我们只能用如下方法使用getters和setters:
class MyClass
{
    // 隐藏属性
 
    private $property;
    // getter
    public function getProperty()
    {
        return $this->property;
    }
    
    // setter
    public function setProperty($value)
    {
        $this->property = $value;
    }
}
 
$object = new MyClass();
// 设置值
$object->setProperty('value');
 
// 获取值
echo $object->getProperty();
  1. 这种语法在Java的世界很常见,但是用在PHP中就有点冗长了。因此,我们想使用C#属性带给我们的同样功能:像类成员一样调用getters和setters。那么就Yii而言,我们可以用下面的方法:
// 继承Ccomponent类是必需的
class MyClass extends CComponent
{
    private $property;
    
    public function getProperty()
    {
        return $this->property;
    }
 
    public function setProperty($value)
    {
        $this->property = $value;
    }
}
 
$object = new MyClass();
$object->property = 'value'; // 和 $object->setProperty('value');  一样
echo $object->property; // 和 $object->getProperty(); 一样
  1. 使用这个特性,你可以使得属性只读或者只写,又能保持这种简洁的PHP语法。
class MyClass extends CComponent
{
    private $read = 'read only property';
    private $write = 'write only property';
    
    public function getRead()
    {
        return $this->read;
    }
    
    public function setWrite($value)
    {
        $this->write = $value;
    }
}
 
$object = new MyClass();
// 这里会报错,因为我们正试图写入只读属性。
$object->read = 'value';
 
// 显示 'read only property'
echo $object->read;
 
// 这里会报错,因为我们正试图读取只写属性。
echo $object->write;
 
// 将 'value' 写入私有变量 $write中去
$object->write = 'value';
  1. Yii深入地使用了这项技术,因为一切皆组件。例如:当你调用Yii::app()->user->id 来获取当前登录用户id,实际上调用的是Yii::app()->getUser()->getId()。

它是如何工作的...

为了允许像上面那种属性风格使用getters和setters,CComponent 使用PHP 魔术方法:__get,__set,__isset,和__unset (http://php.net/manual/en/language.oop5.magic.php)。所以Yii 1.1中CComponent::__get是下面的形式:

public function __get($name)
{
    $getter='get'.$name;
    if(method_exists($this,$getter))
        return $this->$getter();
...

这种魔术PHP方法截获了所有的调用为了漏掉真正的属性,当我们正调用$myClass->property,它接收’property’作为$name参数。如果名为getProperty的方法存在,PHP使用它的返回值作为属性值。

还有更多...

For further information, refer to the following URL:http://www.php.net/manual/en/language.oop5.overloading.php#language. oop5.overloading.members

另请参阅

  • 使用Yii事件处理
  • 配置组件

使用Yii事件

Yii的类大多数都是从CComponent继承的,这使得我们可以通过使用事件来达成应用程序的巨大灵活性。

本章中,你会学到如何在你的应用中定义和使用事件。

怎么做…

因为PHP没有语言级的属性,所以我们只能用如下方法使用getters和setters:

用来定义事件的方法同时一个默认的事件句柄。

典型的件是这样使用的:

  • 添加一个相应的方法来申明事件。
  • 附加一个或多个事件句柄。
  • 组件通过使用CComponent::raiseEvent方法来引发事件。
  • 所有认定的句柄被自动调用。

让我们来看看怎么把一个事件句柄附加到事件上去。我们可以通过CComponent::attachEventHandler方法来实现。它接受2个参数:

  • $name: 事件名
  • $handler: 事件句柄。 这里应该使用一个标准的PHP回调。

在PHP中,我们有几种方法定义一个回调:

  • 使用一个全局函数,只要通过它的名字,像’my_function’这样的字符串。
  • 使用一个静态类方法,你应该传递一个数组: array('ClassName', 'staticMethodName') .
  • 使用对象方法: array($object, 'objectMethod').
  • 你可以创建和传递匿名函数,通过使用create_function:
$component->attachEventHandler('onClick', create_function('$event', 'echo "Click!";'));
  • 从PHP5.3以后,你可以直接使用匿名函数,不需create_function:
$component->attachEventHandler('onClick', function($event){
    echo "Click!";
});

当你使用CComponent::attachEventHandler时候,事件句柄被添加到句柄列表的尾部。

  • 为了使得你的代码精简,你可以使用组件属性来管理这些事件句柄:
$component->onClick=$handler; // 或者:$component->onClick->add($handler);
  • 为了更精确的管理事件句柄,你可以使用CComponent::getEventHandlers来获取句柄列表(CList),并且使用它。例如,你可以用下面的代码来附加事件句柄,跟attachEventHandler一样。
$component->getEventHandlers('onClick')->add($handler);
  • 添加事件句柄到句柄列表头:
$component->getEventHandlers('onClick')->insertAt(0, $handler);
  • 删除具体的句柄,你可以使用CComponent::detachEventHandler:
$component->detachEventHandler('onClick', $handler);
  • 或者像上面的那样获取句柄列表并且从表中删除一些句柄。

CComponent::检查指定的事件是否在组件中定义了。
CComponent::hasEventHandler检查指定的事件是否有句柄依附。

既然我们都知道了怎样定义和使用这些句柄,那么我们来回顾下一些真实的例子吧。

  • 用gzip压缩你的应用输出,来节省客户带宽,来加速页面加载时间,是件司空见惯的事情。如果你有权限能调整你的服务器,你可以让它这样,但是在一些环境下如共享主机,你就不能了。
  • 幸运的是PHP能使用输出缓冲和ob_gzhandler来压缩应用输出。要这样做的话,我们得在应用程序开始的时候开始缓冲输出,并且在它结束的时候释放压缩的输出。
  • 在我们的例子中,Yii应用程序有2个事件迟早有用:CApplication::onBeginRequest和CApplication::onEndRequest。我们现在就来使用它们,将以下代码放置在index.php中的应用程序配置之后,运行之前:
...
require_once($yii);
$app = Yii::createWebApplication($config);
// 在应用程序的开始附加一个句柄
Yii::app()->onBeginRequest = function($event)
{
    // 用ob_gzhandler来开始输出缓冲
    return ob_start("ob_gzhandler");
};
// 附加一个句柄到应用结束
Yii::app()->onEndRequest = function($event)
{
    // 释放输出缓冲
    return ob_end_flush();
};
$app->run();

在Yii核心类中定义了很多便捷的事件。你可以使用你喜爱IDE(集成开发环境)在framework文件夹里面通过“function on”的字样来查找到它们。

我们来看另外一个例子。在Yii中,你可以使用Yii::t来把字符串翻译成不同的语言。我们都追求完美的项目,所以所有的语言翻译都必须是最新的。倘若它们不是,我们将非常乐意收到您的邮件提醒。

在这里事件再次派上用场。尤其是当传递给Yii::t的翻译字符串丢失时调用CMessageSource::onMissingTranslation事件

这次我们使用应用配置文件protected/config/main.php来附加事件句柄:

...
'components' => array(
       ...
       // messages组件类默认的是CPhpMessageSource
       'messages' => array(
        // 使用静态类方法作为事件句柄
       'onMissingTranslation' => array('MyEventHandler',
            'handleMissingTranslation'),
        ),
        ... 
)
...

现在我们应该实现这个句柄,创建protected/components/MyEventHandler.php文件并输入以下代码:

class MyEventHandler
{
    static function handleMissingTranslation($event)
    {
        // 这个事件的事件类是 CMissingTranslationEvent
        // 因此我们能获得这个message的一些信息
        $text = implode("\n", array(
            'Language: '.$event->language,
            'Category:'.$event->category,
            'Message:'.$event->message
        ));
        // 发送邮件
        mail('admin@example.com', 'Missing translation', $text);
    }
}

我们来看最后一个例子。我们有一个博客应用程序,我们需要在有新评论(Comment)提交到博客(Post)的时候就发一份邮件。

Comment是个用Gii生成的标准AR模型。Post也同样是Gii生成的模型,除了一些自定义的方法。我们需要定义一个事件NewCommentEvent来存储Post和Comment 两个模型和句柄类Notifier,它将处理工作。

  1. 让我们开始创建protected/components/NewCommentEvent.php:
class NewCommentEvent extends CModelEvent {
    public $comment;
    public $post;
}

这相当简单。我们只是添加了2个属性。

  1. 现在我们去看看protected/models/Post.php。为了突出显示添加了什么,我们省略了所有标准的AR方法。
class Post extends CActiveRecord {
    // 给添加评论到当前提交上定义方法
    function addComment(Comment $comment){
        $comment->post_id = $this->id;
 
        // 创建事件类实例
        $event = new NewCommentEvent($this);
        $event->post = $this;
        $event->comment = $comment;
        
        // 触发事件
        $this->onNewComment($event);
        return $event->isValid;
    }
 
    // 定义 onNewComment 事件
    public function onNewComment($event) {
        // 事实上事件是在这里触发的.这样的话我们可以使用
        // onNewComment 方法来取代 raiseEvent.
        $this->raiseEvent('onNewComment', $event);
    }
}
  1. 现在,实现Notifie。创建 protected/components/Notifier.php输入如下代码:
class Notifier {
    function comment($event){
        $text = "There was new comment from {$event->comment->author} 
on post {$event->post->title}";
        mail('admin@example.com', 'New comment', $text);
    }
}
  1. 现在是时候在protected/controllers/ PostController.php中一起进行操作了。
class PostController extends CController
{
    unction actionAddComment()
    {
        $post = Post::model()->findByPk(10);
        $notifier = new Notifier();
        
        // 附加事件句柄
        $post->onNewComment = array($notifier, 'comment');
        // 在真实的应用中,数据应来自$_POST
            $comment = new Comment();
            $comment->author = 'Sam Dark';
            $comment->text = 'Yii events are amazing!';
        // 添加评论
            $post->addComment($comment);
    }
}
  1. 所有的评论都将被添加后,管理员将会收到一份相关的邮件。

还有更多...

附加句柄这步不总是必需的。我们来看看怎样处理一个通过重写基类方法定义在已经存在的组件当中的事件。例如,我们有个表格模型UserForm来收集关于我们应用的用户的一些信息,并且我们要求通过用户输入的姓和名来获取完整的姓名。

幸运的,在CModel里面定义有CModel::afterValidate方法,CModel是提供给所有Yii模型的基类,当然包括表单模型了。在成功的表单验证后,这个方法就被调用了。让我们在UserForm模型中使用它吧:

class UserForm extends CFormModel
{
    public $firstName;
    public $lastName;
    public $fullName;
    
    public function rules()
    {
        return array(
            // 姓和名是必填的
            array('firstName, lastName', 'required'),
        );
    }
    
    // 这里的$event参数是CEvent 实例
    // 这个实例在调用事件方法的时候被创建了
    // 在我们的案例,它发生在CModel::afterValidate()里面。
    function afterValidate()
    {
        // 模型已经被填充了有效数据的时候调用这个方法
        // 因此我们安全可靠的使用它:
        $this->fullName = $this->firstName.' '.$this->lastName;
        
        // 把事件传递给父类很重要。
        // 因此其他事件句柄都被调用了。
        return parent::afterValidate();
    }
}

We need to call parent method inside of afterValidate because parent implementation calls onAfterValidate that actually raises events:

protected function afterValidate()
{
    $this->onAfterValidate(new CEvent($this));
}

一个事件方法名应该一直被定义成这个形式:eventHandler($event){...},其中的$event是CEvent的实例。CEvent类就只有2个属性:sender和handled(发送者和已经处理的)。第一个包含一个叫当前事件的对象,而第二个通过设置自己为false来阻止调用其他尚未被执行的句柄。

上面描述的方法能用来定制你的Active Record 模型和实现你模型的行为。

进一步阅读

For further information, refer to the following URLs:

  • http://www.yiiframework.com/doc/api/CComponent/#raiseEvent-detail
  • http://www.yiiframework.com/doc/api/CComponent/#attachEventHandler-detail
  • http://www.yiiframework.com/doc/api/CComponent/#getEventHandlers-detail
  • http://www.yiiframework.com/doc/api/CComponent/#detachEventHandler-detail

另请参阅

  • 使用getters和setters
  • 配置组件

使用导入和自加载

用PHP变成最恼人的事情是用include和require加载额外的代码。幸运的是你可以用SPL类加载器自动完成(http://php.net/manual/en/function.spl-autoload.php)。

Yii紧紧依靠的之一就是不管自加载,话说论坛上有很多相关的问题。咱们把它弄清楚,看看我们怎么利用它。

当我们使用一个类,譬如,CDbCriteria,我们没有明确的包含它,所以PHP起初不能找到它,PHP试着依靠自加载特性。准确来说,就是SPL自加载器。在大部分情况下,Yii默认的加载器(YiiBase::autoload)将被用到。

为了速度和简单,几乎所有的核心框架类都在必要的时候被加载了,而没有明确的包含或导入它们。它是通过YiiBase::$_coreClasses完成的,所以加载核心类是非常迅速的。Zii类像CMenu,扩展类或者你自己写的类都不是自动加载的,因此我们需要先导入它们。

使用Yii::import导入类是相当的明智:

  • 默认的,导入不是立即包含一个类。
  • 没被使用的类不会被包含。
  • 它不会再三的加载一个类,因此你可以放心的导入同一个类很多次。

怎么做…

  1. 假设我们有一个自定义的类LyricsFinder,它负责为给定歌曲找到歌词。我们已经将它放在protected/apis/lyrics/目录下了,在我们的TestController 中,我们打算这样用:
class TestController extends CController
{
    public function actionIndex($song)
    {
        $lyric = 'Nothing was found.';
        $finder = new LyricsFinder();
        
        if(!empty($song))
            $lyric = $finder->getText($song);
        
        echo $lyric;
    }
}
  1. 当我们执行它时,它报出了一个PHP错误:
include(LyricsFinder.php) [<a href='function.include'>function.include</a>]: 
failed to open stream: No such file or directory.

Yii在此帮了我们点忙,在错误显示上,我们可以发现加载器失败是因为Yii找不到我们的类。我们来修改下代码:

class TestController extends CController
{
    public function actionIndex($song)
    {
        $lyric = 'Nothing was found.';
        
        // 导入一个类
        Yii::import('application.apis.lyrics.LyricsFinder');
        
        $finder = new LyricsFinder();
        
        if(!empty($song))
            $lyric = $finder->getText($song);
        
        echo $lyric;
    }
}

现在我们的代码就工作了。

内置的Yii类加载器需要每个类放置在单独的文件中,这些文件和类名称一样。

它是如何工作的...

我们来看看application.apis.lyrics.LyricsFinder:

application是一个标准别名,指向你应用程序的protected文件夹,它被翻译成文件系统路径。下表显示了更多的标准的别名:

AliasPath
applicationpath_to_webroot/protected
systempath_to_webroot/framework
ziipath_to_webroot/framework/zii
webrootpath_to_webroot
extpath_to_webroot/protected/extensions

你可以用Yii::setPathOfAlias方法自己定义你的小应用程序。

apis.lyrics 被转义成apis/lyrics 并且被附加到从小应用程序application中获取的路径;LyricsFinder是个我们想要导入的类名。

如果LyricsFinder需要一些额外的类处于目录中 ,我们可以使用Yii::import(‘application.apis.lyrics.*’)来导入整个目录。 注意* 并不包含子文件夹,因此如果你需要lyrics/includes, 你应该添加另外一条导入语句Yii::import(‘application.apis.lyrics.includes.*).

为了性能,当你使用一个单独的类时,使用明确的路径而不是*比较好。

还有更多...

如果你想你的类像Yii核心类那样自动被导入,你可以在配置文件main.php中配置全局导入:

return array(
    // ...
    
    // global imports
    'import'=>array(
        'application.models.*',
        'application.components.*',
        'application.apis.lyrics.*',
        'application.apis.lyrics.includes.*',
        'application.apis.albums.AlbumFinder',
    ),

注意,跟使用*一样,使用大量的全局导入会减慢你的应用执行速度的。

Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.PacktPub.com. If you purchased this book elsewhere, you can visit http://www.PacktPub. com/support and register to have the files e-mailed directly to you.

使用异常

异常处理是PHP核心特性,但是它们很少被用到,这很不公平。Yii使得异常处理很有用。

Yii异常处理在两个主要方面显得很有用:

  1. 异常处理可以简化检测过程,修复应用错误以及特殊情况如数据库连接失败或者API调用失败。
  2. 异常处理可以干净便捷地生成不同的HTTP响应。

Generally, an exception should be thrown when a component cannot handle a special situation, such as the one said earlier, and needs to leave it to higher-level components.

怎么做…

  1. 设想我们有个使用cURL函数产生响应到API的类application/apis/lyrics/LyricsFinder.php,这个类根据歌名返回一首歌的歌词。下面是我们怎么在其中使用异常处理:
// 制造一些自定义异常,为了特别的来获取它们,如有需要的话
// 一般的歌词查找器异常
class LyricsFinderException extends CException {}
 
// 在有连接问题时候使用
class LyricsFinderHTTPException extends LyricsFinderException{}
 
class LyricsFinder
{
    private $apiUrl = 'http://example.com/lyricsapi&songtitle=%s';
 
    function getText($songTitle)
    {
        $url = $this->getUrl($songTitle);
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        $result = curl_exec($curl);
   
        // 如果有 HTTP错误,我们将抛出一个异常
        if($result===false)
        {
            $errorText = curl_error($curl);
            curl_close($url);
            throw new LyricsFinderHTTPException($errorText);
        }
   
        curl_close($curl);
        return $result;
    }
    
    private function getRequestUrl($songTitle)
    {
        return sprintf($this->apiUrl, urlencode($songTitle));
    }
}
  1. 既然我们不知道某个明确的应用怎样处理API连接问题,所以我们索性丢给应用本身处理好了,它会丢出我们定义的异常LyricsFinderHTTPException。下面是我们怎么在控制器protected/controllers/TestController.php里处理它的:
class TestController extends CController
{
    public function actionIndex($song)
    {
        $lyric = 'Nothing was found.';
 
        // 加载api类
        Yii::import('application.apis.lyrics.LyricsFinder');
 
        $finder = new LyricsFinder();
        
        if(!empty($song))
        {
            // 我们不想把错误显示给用户。
            // 取而代之的是我们想道歉并且邀请他稍后再试一次。
            try {
                $lyric = $finder->getText($song);
            }
            // 我们在此找寻具体的异常
            catch (LyricsFinderHTTPException $e)
            {
                echo 'Sorry, we cannot process your request. Try again later.';
            }
        }
        
        echo $lyric;
       }
}
  1. Yii异常处理的另一个用途是通过抛出CHttpException异常来生成不同的HTTP响应。例如显示一个由Post模型(通过其ID)表现的博客公告的行为可以如下所示:
class PostController extends CController
{
    function actionView()
    {
        if(!isset($_GET['id']))
        // 如果没有提供公告ID,请求明显错误。
        // 根据 HTTP 规格,代码是400错误。
        
        //通过ID查找公告。
        $post = Post::model()->findByPk($_GET['id']);
        if(!$post)
            // 如果没有指定ID的公告,我们将生成HTTP响应:404没找到
            throw new CHttpException(404);
            
            //  如果一切得当,渲染一个公告。
            $this->render('post', array('model' => $post));
    }
}

它是如何工作的...

Yii自动地把所有非致命性应用错误转换为CException异常。

Yii自动地把所有非致命性应用错误转换为CException异常。此外,默认的异常句柄分别引发onError或者onException事件。默认的事件句柄会写一个带有错误级别的日志消息。此外,如果你的应用中YII_DEBUG常量被设置成true,未处理异常或者错误将被显示在手边的错误屏幕上,屏幕显示包括调用堆栈跟踪:发生异常的代码区和文件及所在行数,这样你可以找寻,然后修复。

还有更多...

For further information, refer to the following URLs:

  • http://php.net/manual/en/language.exceptions.php
  • http://www.yiiframework.com/doc/api/CException/
  • http://www.yiiframework.com/doc/api/CHttpException/

配置组件

Yii是个非常可定制的框架。在每个可定制的代码中,应该有一个方便的方法来设置不同的应用部分。Yii中的配置文件名叫main.php,位于protected/config/目录。

怎么做…

如果你以前接触过Yii,那么你大概配置过数据库连接。

return array(
    ...
    'components'=>array(
        'db'=>array(
            'class'=>'system.db.CDbConnection',
            'connectionString'=>'mysql:host=localhost;dbname=database_name',
            'username'=>'root',
            'password'=>'',
            'charset'=>'utf8',
        ),
        ...
    ),
    ...
);

This way of configuring component is used when you want to use a component across all application parts. With the preceding configuration, you can access a component by its name, such as Yii::app()->db.

它是如何工作的...

当你第一次直接或通过活跃记录模型(AR)使用Yii::app()->db组件,Yii创建一个组件并且用应用配置文件main.php中组件部分db数组中相应的值来始化它的公共属性。在先前的代码里’connectionString’的值将被分配给CDbConnection::connectionString,‘username’将被分配给CDbConnection::username,诸如此类。

因此如果你想知道’charset’代表什么或者想知道在CDbConnection类的db组件中还能配置点其他什么的,你应该去查它的API页:http://www.yiiframework.com/doc/api/1.1/CDbConnection/

在前面的代码中,”class”属性有点儿特殊。它不在CDbConnection类中,它可以用来覆写一个类:

return array(
    ...
    'components'=>array(
        'db'=>array(
            'class'=>'application.components.MyDbConnection',
            ...
        ),
        ...
    ),
    ...
);

这种方法可以覆写每一个应用组件,在一个标准组件不适合你的应用的情况下很有用途。

还有更多...

我们来看看哪些标准Yii组件你可以配置。Yii绑定了两种应用类型:

  1. 网络应用(CWebApplication)
  2. 控制台应用

它们2个都是都是继承自CApplication,所以命令行和网络应用都享有CApplication类的组件。

你可以在API页面获取组件名称和registerCoreComponents应用方法的源代码。我们把它们列举在这里作为一个参考。

控制台和网络应用都可以用的组件:

组件名默认/建议的组件类描述
coreMessagesCPhpMessageSource为翻译Yii框架消息提供消息源。
dbCDbConnection提供数据库连接。
messagesCPhpMessageSource为翻译应用消息提供消息源。
errorHandlerCErrorHandler处理PHP错误和未捕获的异常。
securityManagerCSecurityManager提供安全相关的服务,像散列和加密。
statePersisterCStatePersister提供全局状态持续方法。
formatCFormatter提供一套常用的数据格式化方法。
cacheCFileCache提供缓存特性。

额外组件,只有网络应用可用:

组件名默认/建议的组件类描述
coreMessagesCPhpMessageSource为翻译Yii框架消息提供消息源。
dbCDbConnection提供数据库连接。
messagesCPhpMessageSource为翻译应用消息提供消息源。
errorHandlerCErrorHandler处理PHP错误和未捕获的异常。
securityManagerCSecurityManager提供安全相关的服务,像散列和加密。
statePersisterCStatePersister提供全局状态持续方法。
formatCFormatter提供一套常用的数据格式化方法。
cacheCFileCache提供缓存特性。
20%20%40%
组件名默认的组件类描述
sessionCHttpSession提供 session相关的功能。
requestCHttpRequest封装$_SERVER变量且在不同的web服务器中解决它的易变性。
也管理从用户发送和接收的cookies。
urlManagerCUrlManagerURL 路由。用来生成和解析应用URLs。
assetManagerCAssetManager管理私有asset文件的发布。
userCWebUser描绘用户的session信息。
themeManagerCThemeManager管理主题。
authManagerCPhpAuthManager管理基于角色的访问控制(RBAC).
clientScriptCClientScript管理客户端脚本。 (JavaScript and CSS).
widgetFactoryCWidgetFactory创建小挂件并支持小挂件换肤。

你只要简单的添加你的配置项并且把它们的类属性指向你的类就可以添加你自己的应用组件。

另请参阅

  • The recipe named Configuring widget defaults in this chapter

配置挂件默认属性

Yii中,在视图里常使用的代码段都被放在小挂件里。譬如一个挂机可以渲染标签云或者提供自定义的表格输入类型。核心挂机都是高度可配置的,在视图里这样使用:

<?$this->widget('CLinkPager', array(
    'pages' => $pages,
    'pageSize' => 15,
))?>

在这段代码里,我们使用$this->widget调用CLinkPager挂件展示分页,它有一个数组的参数。Pages和pageSize都在被渲染前分配到相应的CLinkPager的公共属性。

注意到我们已经把每页的项数改为了15.如果我们想要分页在我们应用的所有页面的每页都展示15项,我们需要为所有调用的CLinkPager挂件的pageSize参数赋值为15。有更好的办法吗?显然是的。

怎么做…

Yii网络应用提供了许许多多的组件。它们其中的一个是挂件工厂,自从Yii 1.1.3就可以用来设置挂件默认值。

  1. 让我们把pageSize设置大点吧。我们需要编辑应用配置文件main.php:
return array(
    ...
    'components'=>array(
        'widgetFactory'=>array(
            'widgets'=>array(
                'CLinkPager'=>array(
                    'pageSize'=>15,
                ),
                ...
            ),
        ),
        ...
    ),
);
  1. 现在CLinkPager的pageSize的默认值将会是15,所以如果我们在应用里省略pageSize参数,那么在应用程序范围内,它就是15。
  1. 此外,我们还可以为指定挂件覆盖PageSize值:
<?$this->widget('CLinkPager', array(
    'pages' => $pages,
    'pageSize' => 5,
))?>

This works much like the CSS cascade. You are setting the default overall style in an external file, but are still able to override this through inline styles for individual widgets.

另请参阅

  • 配置组件

使用Yii核心集合组件

Yii有一组集合组件类,主要为内部用途使用。虽然未在权威指南里描述,却对应用开发很有帮助:

  • Lists: CList, CTypedList
  • Maps: CMap, CAttributeCollection
  • Queue: CQueue
  • Stack: CStack

怎么做…

所有的集合挂件都实现了SPL IteratorAggregate, Traversable 和 Countable。 Lists和maps也实现了SPL ArrayAccess,它允许使用集合如同标准PHP变量。下面是CList API中的一段代码:

  • 下面是CList API中的一段代码:
// 在尾部追加
$list[]=$item;
 
// $index必须在 0 和 $list->Count之间
$list[$index]=$item;
 
// 清空 $index的项
unset($list[$index]);
 
// 如果列表中有$index的项
if(isset($list[$index]))
 
    // 遍历列表中的每一项
    foreach($list as $index=>$item)
        // 返回列表项的数目
        $n=count($list);
  • CList 是个整数索引的集组件和传统的PHP数组相比,它加入了更严格的检查,能够被用到面向对象风格并允许集合只读:
$list = new CList();
$list->add('python');
$list->add('php');
$list->add('java')
if($list->contains('php'))
    $list->remove('java');
    
$anotherList = new CList(array('python', 'ruby'));
$list->mergeWith($anotherList);
 
$list->setReadOnly(true);
 
print_r($list->toArray());
  • 有另外一个列表集合物件叫CTypedList ,它确保列表只包含某一类型的项:
$typedList = new CTypedList('Post');
$typedList->add(new Post());
$typedList->add(new Comment());

因为我们试图添加评论到公告列表,所以之前的代码会给你以下的异常:

CTypedList<Post> can only hold objects of Post class.
  • CMap允许使用每个值作为键,不管是否整型。和Clist一样,它也可以以原始PHP风格使用,有几乎相同的一套面向对象方法,允许集合物件只读:
$map = new CMap();
$map->add('php', array('facebook', 'wikipedia', 'wordpress',
       'drupal'));
$map->add('ruby', array('basecamp', 'twitter'));
print_r($map->getKeys());
  • 也有个便利的静态方法CMap::mergeArray,可以递归使用来合并两个关联的数组,且替换了标量的值:
$apps1 = array(
    'apps' => array(
        'task tracking',
        'bug tracking',
    ),
    'is_new' => false
);
 
$apps2 = array(
    'apps' => array(
        'blog',
        'task tracking',
    ),
    'todo' => array(
        'buy milk',
    ),
    'is_new' => true
);
 
$apps = CMap::mergeArray($apps1, $apps2);
CVarDumper::dump($apps, 10, true);

The result of the preceding code is as follows:

array
(
    'apps' => array
    (
        '0' => 'task tracking'
        '1' => 'bug tracking'
        '2' => 'blog'
        '3' => 'task tracking'
    ),
    'is_new' => true,
    'todo' => array
    (
        '0' => 'buy milk'
    )
)
  • CAttributeCollection includes all CMap functionality and can work with data just like properties:
$col = new CAttributeCollection();
 
// $col->add('name','Alexander');
$col->name='Alexander';
 
// echo $col->itemAt('name');
echo $col->name;
  • CQueue and CStack implement a queue and a stack respectively. A stack works as LIFO: last in, first out, and the queue is FIFO: first in, first out. Same as list and map collections these can be used in native PHP style and have OO style methods:
$queue = new CQueue();
 
// add some tasks
$queue->enqueue(new Task('buy milk'));
$queue->enqueue(new Task('feed a cat'));
$queue->enqueue(new Task('write yii cookbook'));
 
// complete a task (remove from queue and return it)
echo 'Done with '.$queue->dequeue();
echo count($queue).' items left.';
// return next item without removing it
echo 'Next one is '.$queue->peek();
 
foreach($queue as $task)
    print_r($task);
 
$garage = new CStack();
 
// getting some cars into the garage
$garage->push(new Car('Ferrari'));
$garage->push(new Car('Porsche'));
$garage->push(new Car('Kamaz'));
 
// Ferrari and Porsche can't get out
// since there is...
echo $garage->peek(); // Kamaz!
 
// we need to get Kamaz out first
$garage->pop();
$porsche = $garage->pop();
$porsche->drive();

处理请求

你可以使用PHP超级全局变量像$_SERVER,$_GET或者$_POST来直接响应请求数据,当是更好的方法是使用Yii强大的CHttpRequest类,它解决了在各种不同服务器中的不一致性,管理cookies,提供一些额外的安全性和一套给力的面向对象方法。

怎么做…

你可以使用Yii::app()->getRequest()访问你应用中的请求组件,我们来回顾下最有用的一些方法和它们的用途。 这些方法返回当前URL的不同部分。在下面的表格中,我们用粗体字样标记了返回的部分。

getUrlhttp://yiibook.local/test/index?var=val
getHostInfohttp://yiibook.local/test/index?var=val
getPathInfohttp://yiibook.local/test/index?var=val
getRequestUrihttp://yiibook.local/test/index?var=val
getQueryStringhttp://yiibook.local/test/index?var=val

允许我们确保请求类型的方法是getIsPostRequest, getIsAjaxRequest, and getRequestType。

  • 例如,我们可以使用getIsAjaxRequest来根据请求类型响应不同的内容:
class TestController extends CController
{
    public function actionIndex()
    {
        if(Yii::app()->request->isAjaxRequest)s
            $this->renderPartial('test');
        else
            $this->render('test');
    }
}

在上面的代码中,如果请求是通过AJAX,我们就渲染了一个没有布局的视图。

  • 尽管PHP为POST和GET都提供了超全局变量,但Yii的对应方法则允许我们省略一些额外的检查:
class TestController extends CController
{
    public function actionIndex()
    {
        $request = Yii::app()->request;
        
        $param = $request->getParam('id', 1);
        // 等价于
        $param = isset($_REQUEST['id']) ? $_REQUEST['id'] : 1;
        
        $param = $request->getQuery('id');
        // 等价于
        $param = isset($_GET['id']) ? $_GET['id'] : null;
        
        $param = $request->getPost('id', 1);
        // 等价于
        $param = isset($_POST['id']) ? $_POST['id'] : 1;
    }
}
  • getPreferredLanguage试图确定用户偏爱的语言。 它不够完全地精确,但是在用户没有手动规定首选语言的时候,它就是十分好的依靠。.
class TestController extends CController
{
    public function actionIndex()
    {
        $request = Yii::app()->request;
        $lang = $request->preferredLanguage;
        
        // 试着从DB里获取语言设定
        $criteria = new CDbCriteria();
        $criteria->compare('user_id', $request->getQuery('userid'));
        $criteria->compare('key', 'language');
        $setting = Settings::model()->find($criteria);
        if($setting)
            $lang = $setting->value;
 
        Yii::app()->setLanguage($lang);
        
        echo Yii::t('app', 'Language is: ').$lang;
    }
}
  • sendfile可以启动文件下载:
class TestController extends CController
{
    public function actionIndex()
    {
        $request = Yii::app()->getRequest();
        $request->sendFile('test.txt', 'File content goes here.');
    }
}

这个操作方法将触发文件下载并且发送所有必需的头信息,这些头信息涵括文档的内容类型(mimetype)和内容长度。如果没有手动的设置为第三个参数Mimetype,就会被基于文件名扩展名推测。

  • 本章中我们最后要看的东西是getCookies方法。它返回一个CCookieCollection类实例,这个实例允许我们处理cookies。因为CCookieCollection继承自CMap,所以我们可以使用一些原始的PHP方法:
class TestController extends CController
{
    public function actionIndex()
    {
        $request = Yii::app()->request;
        // 获取cookie
        $cookie = $request->cookies['test'];
        if($cookie)
            // 打印 cookie 值
            echo $cookie->value;
        else {
            // 创建一个cookie实例
            $cookie=new CHttpCookie('test','I am a cookie!');
            $request->cookies['test'] = $cookie;
        }
    }
}

还有更多...

如果你有很多cookie值要处理,并且想精简所提供的代码,你可以使用像这样的助手方法:

class Cookie
{
    public static function get($name)
    {
        $cookie=Yii::app()->request->cookies[$name];
        if(!$cookie)
            return null;
       return $cookie->value;
    }
    
    public static function set($name, $value, $expiration=0)
    {
        $cookie=new CHttpCookie($name,$value);
        $cookie->expire = $expiration;
        Yii::app()->request->cookies[$name]=$cookie;
    }
}

在你把这份代码放到protected/components/Cookie.php后,你将可以这样做:

class TestController extends CController
{
    public function actionIndex()
    {
        $cookie = Cookie::get('test');
        if($cookie)
            echo $cookie;
        else
            Cookie::set('test','I am a cookie!!');
    }
}
评论 X

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