yii第一個應用blog

1. 鏈接到數據庫 

大多數 Web 應用由數據庫驅動,咱們的測試應用也不例外。要使用數據庫,咱們首先須要告訴應用如何鏈接它。修改應用的配置文件 WebRoot/testdrive/protected/config/main.php 便可,以下所示:php

return array(
    ......
    'components'=>array(
        ......
        'db'=>array(
            'connectionString'=>'sqlite:protected/data/source.db',
        ),
    ),
    ......
);

上面的代碼告訴 Yii 應用在須要時將鏈接到 SQLite 數據庫。mysql

如何使用mysql:ajax

Tip: If you want to use MySQL instead of SQLite to store data, you may cre-
ate a MySQL database named blog using the SQL statements in /wwwroot/yii/
demos/blog/protected/data/schema.mysql.sql. Then, modify the application
con guration as follows,
return array(
......
'components'=>array(
......
'db'=>array(
'connectionString' => 'mysql:host=localhost;dbname=blog',
'emulatePrepare' => true,
'username' => 'root',
'password' => '',
'charset' => 'utf8',
'tablePrefix' => 'tbl ',
),
),
......
);sql

2. 實現 CRUD 操做 

激動人心的時刻來了。咱們想要爲剛纔創建的 tbl_user 表實現 CRUD (create, read, update 和 delete) 操做,這也是實際應用中最多見的操做。咱們無需麻煩地編寫實際代碼,這裏咱們將使用 Gii —— 一個強大的基於Web 的代碼生成器。 數據庫

配置Gii

爲了使用 Gii,首先須要編輯文件 WebRoot/testdrive/protected/main.php,這是已知的 應用配置 文件:api

return array(
    ......
    'import'=>array(
        'application.models.*',
        'application.components.*',
    ),
 
    'modules'=>array(
        'gii'=>array(
            'class'=>'system.gii.GiiModule',
            'password'=>'pick up a password here',
        ),
    ),
);

而後,訪問 URL http://hostname/testdrive/index.php?r=gii。這裏咱們須要輸入密碼,它是在咱們在上面的配置中指定的。瀏覽器

生成 User 模型

登錄後,點擊連接 Model Generator。它將顯示下面的模型生成頁面,安全

在 Table Name 輸入框中,輸入 tbl_user。在 Model Class 輸入框中,輸入 User。而後點擊 Preview 按鈕。這裏將展現將要生成的新文件。如今點擊 Generate 按鈕。一個名爲 User.php 將生成到protected/models 目錄中。如咱們稍後描述的, User 模型類容許咱們以面向對象的方式來訪問數據表tbl_user 。app

Repeat the same procedure for the rest of the database tables, including tbl post, tbl
comment, tbl tag and tbl lookup.框架

Tip: We can also enter an asterisk character * in the Table Name Field. This will
generate a model class for every database table in a single shot.

3. Implementing CRUD Operations
After the model classes are created, we can use the Crud Generator to generate the code
implementing the CRUD operations for these models. We will do this for the Post and
Comment models.

On the Crud Generator page, enter Post (the name of the post model class we just created)
in the Model Class eld, and then press the Preview button. We will see a lot more les
will be generated. Press the Generate button to generate them.
Repeat the same procedure for the Comment model.

 

Let's take a look at the fi les generated by the CRUD generator. All the fi les are generated
under /wwwroot/blog/protected. For convenience, we group them into controller fi les and
view fi les:

Testing
 We can test the features implemented by the code we just generated by accessing the
following URLs:
http://www.example.com/blog/index.php?r=post
http://www.example.com/blog/index.php?r=comment

 主要post和comment,因爲代碼是自動生成的,因此2者尚未關聯起來。

Authenticating User
  Our blog application needs to differentiate between the system owner and guest users.
Therefore, we need to implement the user authentication feature.As you may have found that the skeleton application already provides user authentication by checking if the username and password are both demo or admin. In this section, we
will modify the corresponding code so that the authentication is done against the User  database table.

User authentication is performed in a class implementing the [IUserIdentity] interface.
The skeleton application uses the UserIdentity class for this purpose. The class is stored
in the file /wwwroot/blog/protected/components/UserIdentity.php.

Tip: By convention, the name of a class file must be the same as the corresponding
class name suffixed with the extension .php. Following this convention, one can
refer to a class using a path alias. For example, we can refer to the UserIdentity
class with the alias application.components.UserIdentity. Many APIs in Yii
can recognize path aliases (e.g. Yii::createComponent()), and using path aliases
avoids the necessity of embedding absolute file paths in the code. The existence of
the latter often causes trouble when we deploy an application.

默認的代碼以下:

class UserIdentity extends CUserIdentity
{
    /**
     * Authenticates a user.
     * The example implementation makes sure if the username and password
     * are both 'demo'.
     * In practical applications, this should be changed to authenticate
     * against some persistent user identity storage (e.g. database).
     * @return boolean whether authentication succeeds.
     */
    public function authenticate()
    {
        $users=array(
            // username => password
            'demo'=>'demo',
            'admin'=>'admin',
        );
        if(!isset($users[$this->username]))
            $this->errorCode=self::ERROR_USERNAME_INVALID;
        elseif($users[$this->username]!==$this->password)
            $this->errorCode=self::ERROR_PASSWORD_INVALID;
        else
            $this->errorCode=self::ERROR_NONE;
        return !$this->errorCode;
    }
}

 

咱們將 UserIdentity 類作以下修改,

<?php
class UserIdentity extends CUserIdentity
{
    private $_id;
 
    public function authenticate()
    {
        $username=strtolower($this->username);
        $user=User::model()->find('LOWER(username)=?',array($username));
        if($user===null)
            $this->errorCode=self::ERROR_USERNAME_INVALID;
        else if(!$user->validatePassword($this->password))
            $this->errorCode=self::ERROR_PASSWORD_INVALID;
        else
        {
            $this->_id=$user->id;
            $this->username=$user->username;
            $this->errorCode=self::ERROR_NONE;
        }
        return $this->errorCode==self::ERROR_NONE;
    }
 
    public function getId()
    {
        return $this->_id;
    }
}

在 authenticate() 方法中,咱們使用 User 類來查詢 tbl_user 表中 username 列值(不區分大小寫)和提供的用戶名一致的一行,請記住 User 類是在前面的章節中經過 gii 工具建立的。因爲 User 類繼承自CActiveRecord ,咱們能夠利用 ActiveRecord 功能 以 OOP 的風格訪問 tbl_user 表。

爲了檢查用戶是否輸入了一個有效的密碼,咱們調用了 User 類的 validatePassword 方法。咱們須要按下面的代碼修改 /wwwroot/blog/protected/models/User.php 文件。注意,咱們在數據庫中存儲了密碼的加密串和隨機生成的SALT密鑰,而不是存儲明文密碼。 因此當要驗證用戶輸入的密碼時,咱們應該和加密結果作對比。

class User extends CActiveRecord
{
    ......
    public function validatePassword($password)
    {
        return $this->hashPassword($password,$this->salt)===$this->password;
    }
 
    public function hashPassword($password,$salt)
    {
        return md5($salt.$password);
    }
}

在 UserIdentity 類中,咱們還覆蓋(Override,又稱爲重寫)了 getId() 方法,它會返回在 User 表中找到的用戶的 id。父類 (CUserIdentity) 則會返回用戶名。username 和 id 屬性都將存儲在用戶 SESSION 中,可在代碼的任何部分經過 Yii::app()->user 訪問。

提示: 在 UserIdentity 類中,咱們沒有顯式包含(include)相應的類文件就訪問了 CUserIdentity 類,這是由於 CUserIdentity 是一個由Yii框架提供的核心類。Yii 將會在任何核心類被首次使用時自動包含類文件。

咱們也對 User 類作了一樣的事情。這是由於 User 類文件被放在了/wwwroot/blog/protected/models 目錄,此目錄已經經過應用配置中的以下幾行代碼被添加到了 PHP 的 include_path 中:

return array(
    ......
    'import'=>array(
        'application.models.*',
        'application.components.*',
    ),
    ......
);

上面的配置說明,位於 /wwwroot/blog/protected/models 或/wwwroot/blog/protected/components 目錄中的任何類將在第一次使用時被自動包含。

UserIdentity 類主要用於 LoginForm 類中,它基於用戶名和從登陸頁中收到的密碼來實現用戶驗證。下面的代碼展現了 UserIdentity 的使用:

$identity=new UserIdentity($username,$password);
$identity->authenticate();
switch($identity->errorCode)
{
    case UserIdentity::ERROR_NONE:
        Yii::app()->user->login($identity);
        break;
    ......
}

信息: 人們常常對 identity 和 user 應用組件感到困惑,前者表明的是一種驗證方法,後者表明當前用戶相關的信息。一個應用只能有一個 user 組件,但它能夠有一個或多個 identity 類,這取決於它支持什麼樣的驗證方法。一旦驗證經過,identity 實例會把它本身的狀態信息傳遞給 user 組件,這樣它們就能夠經過 user 實現全局可訪問。

 

model中rules() 方法中定義的規則會在模型實例調用其 validate() 或 save() 方法時逐一執行。注意: 請務必記住 rules() 中出現的屬性必須是那些經過用戶輸入的屬性。其餘的屬性,如 Post 模型中的 id 和 create_time ,是經過咱們的代碼或數據庫設定的,不該該出如今 rules() 中。詳情請參考 屬性的安全賦值(Securing Attribute Assignments).

 

自定義 relations() 方法 

最後咱們來自定義 relations() 方法,以指定與日誌相關的對象。經過在 relations() 中聲明這些相關對象,咱們就能夠利用強大的 Relational ActiveRecord (RAR) 功能來訪問日誌的相關對象,例如它的做者和評論。不須要本身寫複雜的 SQL JOIN 語句。

咱們自定義 relations() 方法以下:

public function relations()
{
    return array(
        'author' => array(self::BELONGS_TO, 'User', 'author_id'),
        'comments' => array(self::HAS_MANY, 'Comment', 'post_id',
            'condition'=>'comments.status='.Comment::STATUS_APPROVED,
            'order'=>'comments.create_time DESC'),
        'commentCount' => array(self::STAT, 'Comment', 'post_id',
            'condition'=>'status='.Comment::STATUS_APPROVED),
    );
}

咱們還在 Comment 模型類中定義了兩個在上面的方法中用到的常量。

class Comment extends CActiveRecord
{
    const STATUS_PENDING=1;
    const STATUS_APPROVED=2;
    ......
}

relations() 中聲明的關係代表:

  • 一篇日誌屬於一個做者,它的類是 User ,它們的關係創建在日誌的 author_id 屬性值之上;
  • 一篇日誌有多個評論,它們的類是 Comment ,它們的關係創建在評論的 post_id 屬性值之上。這些評論應該按它們的建立時間排列,且評論必須已經過審覈;
  • commentCount 關係有一點特別,它返回一個關於日誌有多少條評論的一個聚合結果。

經過以上的關係聲明,咱們如今能夠按下面的方式很容易的訪問日誌的做者和評論信息。

$author=$post->author;
echo $author->username;
 
$comments=$post->comments;
foreach($comments as $comment)
    echo $comment->content;

關於如何聲明和使用關係的更多詳情,請參考 指南.

 

class Post extends CActiveRecord
{
    public function getUrl()
    {
        return Yii::app()->createUrl('post/view', array(
            'id'=>$this->id,
            'title'=>$this->title,
        ));
    }
}

注意咱們除了使用日誌的ID以外,還添加了日誌的標題做爲URL中的一個 GET 參數。這主要是爲了搜索引擎優化 (SEO) 的目的,在 美化 URL 中將會講述。

因爲 CComponent 是 Post 的最頂級父類,添加 getUrl() 這個 getter 方法使咱們可使用相似 $post->url 這樣的表達式。當咱們訪問 $post->url 時,getter 方法將會被執行,它的返回結果會成爲此表達式的值。關於這種組件的更多詳情,請參考 指南

 

postController中access rules:

accessRules()

咱們將 /wwwroot/blog/protected/controllers/PostController.php 文件中的 accessRules() 方法修改以下:

public function accessRules()
{
    return array(
        array('allow',  // allow all users to perform 'list' and 'show' actions
            'actions'=>array('index', 'view'),
            'users'=>array('*'),
        ),
        array('allow', // allow authenticated users to perform any action
            'users'=>array('@'),
        ),
        array('deny',  // deny all users
            'users'=>array('*'),
        ),
    );
}

上面的規則說明:全部用戶都可訪問 index 和 view 動做,已經過身份驗證的用戶能夠訪問任意動做,包括admin 動做。在其餘場景中,應禁止用戶訪問。注意這些規則將會按它們在此列出的順序計算。第一條匹配當前場景的規則將決定訪問權。例如,若是當前用戶是系統全部者,他想嘗試訪問日誌建立頁,第二條規則將匹配成功並授予此用戶權限。

 

2. 自定義 建立 和 更新 操做 

建立 和 更新 操做很是類似。他們都須要顯示一個HTML表單用於收集用戶的輸入的信息,而後對其進行驗證,而後將其存入數據庫。主要的不一樣是 更新 操做須要把從數據庫找到的已存在的日誌數據重如今表單中。鑑於此,yiic 工具建立了一個局部視圖 /wwwroot/blog/protected/views/post/_form.php ,它會插入 建立 和更新 視圖來渲染所需的HTML表單

咱們先修改 _form.php 這個文件,使這個HTML表單只收集咱們想要的輸入:titlecontenttags 和status。咱們使用文本域收集前三個屬性的輸入,還有一個下拉列表用來收集 status 的輸入。此下拉列表的選項值就是可用的日誌狀態文本。

<?php echo $form->dropDownList($model,'status',Lookup::items('PostStatus')); ?>

在上面的代碼中,咱們調用了 Lookup::items('PostStatus') 以帶回日誌狀態列表。

而後我們修改 Post 類,使它能夠在日誌被存入數據庫前自動設置幾個屬性 (例如 create_timeauthor_id)。咱們覆蓋 beforeSave() 方法以下:

protected function beforeSave()
{
    if(parent::beforeSave())
    {
        if($this->isNewRecord)
        {
            $this->create_time=$this->update_time=time();
            $this->author_id=Yii::app()->user->id;
        }
        else
            $this->update_time=time();
        return true;
    }
    else
        return false;
}

當咱們保存日誌時,咱們想更新 tbl_tag 表以反映 Tag 的使用頻率。咱們能夠在 afterSave() 方法中完成此工做,它會在日誌被成功存入數據庫後自動被Yii調用。

protected function afterSave()
{
    parent::afterSave();
    Tag::model()->updateFrequency($this->_oldTags, $this->tags);
}
 
private $_oldTags;
 
protected function afterFind()
{
    parent::afterFind();
    $this->_oldTags=$this->tags;
}

在這個實現中,由於咱們想檢測出用戶在更新現有日誌的時候是否修改了 Tag ,咱們須要知道原來的 Tag 是什麼, 鑑於此,咱們還寫了一個 afterFind() 方法把原有的 Tag 信息保存到變量 _oldTags 中。方法 afterFind()會在一個 AR 記錄被數據庫中的數據填充時自動被 Yii 調用。

這裏咱們再也不列出 Tag::updateFrequency() 方法的細節,讀者能夠參考/wwwroot/yii/demos/blog/protected/models/Tag.php 文件。

 

控制器

 

 自定義 view 操做 

view 操做是經過 PostController 中的 actionView() 方法實現的。它的顯示是經過 view 視圖文件/wwwroot/blog/protected/views/post/view.php 生成的。

下面是在 PostController 中實現 view 操做的具體代碼:

public function actionView()
{
    $post=$this->loadModel();
    $this->render('view',array(
        'model'=>$post,
    ));
}
 
private $_model;
 
public function loadModel()
{
    if($this->_model===null)
    {
        if(isset($_GET['id']))
        {
            if(Yii::app()->user->isGuest)
                $condition='status='.Post::STATUS_PUBLISHED
                    .' OR status='.Post::STATUS_ARCHIVED;
            else
                $condition='';
            $this->_model=Post::model()->findByPk($_GET['id'], $condition);
        }
        if($this->_model===null)
            throw new CHttpException(404,'The requested page does not exist.');
    }
    return $this->_model;
}

 

2. 自定義 index 操做 

和 view 操做相似,咱們在兩處自定義 index 操做:PostController 中的 actionIndex() 方法和視圖文件/wwwroot/blog/protected/views/post/index.php。咱們主要須要添加對顯示一個特定Tag下的日誌列表的支持;

下面就是在 PostController 中對 `actionIndex() 方法做出的修改:

public function actionIndex()
{
    $criteria=new CDbCriteria(array(
        'condition'=>'status='.Post::STATUS_PUBLISHED,
        'order'=>'update_time DESC',
        'with'=>'commentCount',
    ));
    if(isset($_GET['tag']))
        $criteria->addSearchCondition('tags',$_GET['tag']);
 
    $dataProvider=new CActiveDataProvider('Post', array(
        'pagination'=>array(
            'pageSize'=>5,
        ),
        'criteria'=>$criteria,
    ));
 
    $this->render('index',array(
        'dataProvider'=>$dataProvider,
    ));
}

在上面的代碼中,咱們首先爲檢索日誌列表建立了一個查詢標準(criteria),此標準規定只返回已發佈的日誌,且應該按其更新時間倒序排列。由於咱們打算在顯示日誌列表的同時顯示日誌收到的評論數量,所以在這個標準中咱們還指定了要帶回 commentCount, 若是你還記得,它就是在 Post::relations() 中定義的一個關係。

考慮到當用戶想查看某個Tag下的日誌列表時的狀況,咱們還要爲指定的Tag添加一個搜索條件到上述標準中。

使用這個查詢標準,咱們建立了一個數據提供者(data provider)。這主要出於三個目的。第一,它會在查詢結果過多時實現數據分頁。這裏咱們定義分頁的頁面大小爲5。 第二,它會按用戶的請求對數據排序。最後,它會填充排序並分頁後的數據到小部件(widgets)或視圖代碼用於顯示。

完成 actionIndex() 後,咱們將 index 視圖修改成以下代碼。 此修改主要是關於在用戶指定顯示Tag下的日誌時添加一個 h1 標題。

<?php if(!empty($_GET['tag'])): ?>
<h1>Posts Tagged with <i><?php echo CHtml::encode($_GET['tag']); ?></i></h1>
<?php endif; ?>
 
<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
    'template'=>"{items}\n{pager}",
)); ?>

注意上面的代碼,咱們使用了 CListView 來顯示日誌列表。這個小物件須要一個局部視圖以顯示每一篇日誌的詳情。這裏咱們制定了局部視圖爲 _view,也就是文件 /wwwroot/blog/protected/views/post/_view.php. 在這個視圖腳本中,咱們能夠經過一個名爲 $data 的本地變量訪問顯示的日誌實例。

參考資料

 

日誌管理 

 

日誌管理主要是在一個管理視圖中列出日誌,咱們能夠查看全部狀態的日誌,更新或刪除它們。它們分別經過admin 操做和 delete 操做實現。yiic 生成的代碼並不須要太多修改。下面咱們主要解釋這兩個操做是怎樣實現的。

1. 在表格視圖中列出日誌 

admin 操做在一個表格視圖中列出了全部狀態的日誌。此視圖支持排序和分頁。下面就是 PostController 中的actionAdmin() 方法:

public function actionAdmin()
{
    $model=new Post('search');
    if(isset($_GET['Post']))
        $model->attributes=$_GET['Post'];
    $this->render('admin',array(
        'model'=>$model,
    ));
}

上面的代碼由 yiic 工具生成,且未做任何修改。它首先建立了一個 search 場景(scenario) 下的 Post 模型。咱們將使用此模型收集用戶指定的搜索條件。而後咱們把用戶可能會提供的數據賦值給模型。 最後,咱們以此模型顯示 admin 視圖。

下面就是 admin 視圖的代碼:

<?php
$this->breadcrumbs=array(
    'Manage Posts',
);
?>
<h1>Manage Posts</h1>
 
<?php $this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$model->search(),
    'filter'=>$model,
    'columns'=>array(
        array(
            'name'=>'title',
            'type'=>'raw',
            'value'=>'CHtml::link(CHtml::encode($data->title), $data->url)'
        ),
        array(
            'name'=>'status',
            'value'=>'Lookup::item("PostStatus",$data->status)',
            'filter'=>Lookup::items('PostStatus'),
        ),
        array(
            'name'=>'create_time',
            'type'=>'datetime',
            'filter'=>false,
        ),
        array(
            'class'=>'CButtonColumn',
        ),
    ),
)); ?>

咱們使用 CGridView 來顯示這些日誌。它容許咱們在單頁顯示過多時能夠分頁並能夠按某一列排序。咱們的修改主要針對每一列的顯示。例如,針對 title 列,咱們指定它應該顯示爲一個超級連接,指向日誌的詳情頁面。表達式$data->url 返回咱們以前在 Post 類中定義的 url 屬性值。

提示: 當顯示文本時,咱們要調用 CHtml::encode() 對其中的HTML編碼。這能夠防止 跨站腳本攻擊(cross-site scripting attack).

2. 日誌刪除 

在 admin 數據表格中,每行有一個刪除按鈕。點擊此按鈕將會刪除相應的日誌。在程序內部,這會觸發以下實現的 delete 動做。

public function actionDelete()
{
    if(Yii::app()->request->isPostRequest)
    {
        // we only allow deletion via POST request
        $this->loadModel()->delete();
 
        if(!isset($_POST['ajax']))
            $this->redirect(array('index'));
    }
    else
        throw new CHttpException(400,'Invalid request. Please do not repeat this request again.');
}

上面的代碼就是 yiic 生成的代碼,未經任何修改。咱們想在此對判斷 $_POST['ajax'] 稍做解釋。CGridView小物件有一個很是好的特性:它的排序、分頁和刪除操做默認是經過AJAX實現的。這就意味着在執行上述操做時,整個頁面不會從新加載。然而,它也能夠在非AJAX模式下運行(經過設置它的 ajaxUpdate 屬性爲 false 或在客戶端禁用JavaScript)。delete 動做區分兩個場景是必要的:若是刪除請求經過AJAX提交,咱們就不該該重定向用戶的瀏覽器,反之則應該重定向。

刪除日誌應該同時致使日誌的全部評論被刪除。額外的,咱們應更新相關的刪除日誌後的 tbl_tag 表。 這兩個任務均可以經過在 Post 模型類中寫一個以下的 afterDelete 方法實現。

protected function afterDelete()
{
    parent::afterDelete();
    Comment::model()->deleteAll('post_id='.$this->id);
    Tag::model()->updateFrequency($this->tags, '');
}

上面的代碼很直觀:它首先刪除了全部 post_id 和所刪除的日誌ID相同的那些評論。而後它針對所刪日誌中的tags 更新了 tbl_tag 表。

提示: 因爲 SQLite 並不真正支持外鍵約束,咱們須要顯式地刪除屬於所刪日誌的全部評論。在一個支持此約束的DBMS (例如 MySQL, PostgreSQL)中,能夠設置好外鍵約束,這樣若是刪除了一篇日誌,DBMS就能夠自動刪除其評論。這樣的話,咱們就不須要在咱們的代碼中顯式執行刪除了。

 
 
Comment評論:

自定義存儲的流程 

因爲咱們想要記錄評論建立的時間,和咱們在 Post 模型中的作法同樣,咱們覆蓋 Comment 的 beforeSave()方法以下:

protected function beforeSave()
{
    if(parent::beforeSave())
    {
        if($this->isNewRecord)
            $this->create_time=time();
        return true;
    }
    else
        return false;
}
相關文章
相關標籤/搜索