Building A Blog System Using Yii: Qiang Xue
Building A Blog System Using Yii: Qiang Xue
Building A Blog System Using Yii: Qiang Xue
Qiang Xue
Contents i
License v
1 Getting Started 1
2 Initial Prototyping 9
2.2 Scaffolding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3 Post Management 19
ii Contents
4 Comment Management 33
5 Portlets 41
5.1.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
6 Final Work 47
6.4.2 Internationalization . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
The Yii framework is free software. It is released under the terms of the following BSD
License.
Copyright
2008-2010
c by Yii Software LLC. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
3. Neither the name of Yii Software LLC nor the names of its contributors may be
used to endorse or promote products derived from this software without specific
prior written permission.
Getting Started
Readers of this tutorial are not required to have prior knowledge about Yii. However, basic
knowledge of object-oriented programming (OOP) and database programming would help
readers to understand the tutorial more easily.
We first install the Yii framework. Grab a copy of the Yii release file (version 1.1.1 or
above) from www.yiiframework.com and unpack it to the directory /wwwroot/yii. Double
check to make sure that there is a directory /wwwroot/yii/framework.
Tip: The Yii framework can be installed anywhere in the file system, not necessarily
under a Web folder. Its framework directory contains all framework code and is
the only framework directory needed when deploying an Yii application. A single
installation of Yii can be used by multiple Yii applications.
2 1. Getting Started
After installing Yii, open a browser window and access the URL http://www.example.
com/yii/requirements/index.php. It shows the requirement checker provided in the Yii
release. For our blog application, besides the minimal requirements needed by Yii, we also
need to enable both the pdo and pdo sqlite PHP extensions so that we can access SQLite
databases.
We then use the yiic tool to create a skeleton application under the directory /wwwroot/
blog. The yiic tool is a command line tool provided in the Yii release. It can be used to
generate code to reduce certain repetitive coding tasks.
Tip: In order to use the yiic tool as shown above, the CLI PHP program must be
on the command search path. If not, the following command may be used instead:
To try out the application we just created, open a Web browser and navigate to the URL
http://www.example.com/blog/index.php. We should see that our skeleton application
already has four fully functional pages: the homepage, the about page, the contact page
and the login page.
Entry Script
We have an entry script file /wwwroot/blog/index.php which has the following content:
<?php
$yii=’/wwwroot/framework/yii.php’;
$config=dirname( FILE ).’/protected/config/main.php’;
require once($yii);
Yii::createWebApplication($config)->run();
This is the only script that Web users can directly access. The script first includes the
Yii bootstrap file yii.php. It then creates an application instance with the specified
configuration and executes the application.
For other Web servers, please refer to the corresponding manual on how to protect a
directory from being accessed by Web users.
To help understand how Yii works, we describe the main workflow in our skeleton appli-
cation when a user is accessing its contact page:
2. The entry script is executed by the Web server to process the request;
3. An application instance is created and configured with initial property values speci-
fied in the application configuration file /wwwroot/blog/protected/config/main.php;
4. The application resolves the request into a controller and a controller action. For the
contact page request, it is resolved as the site controller and the contact action (the
actionContact method in /wwwroot/blog/protected/controllers/SiteController.php);
6. The SiteController instance executes the contact action by calling its actionContact()
method;
4 1. Getting Started
7. The actionContact method renders a view named contact to the Web user. Inter-
nally, this is achieved by including the view file /wwwroot/blog/protected/views/
site/contact.php and embedding the result into the layout file /wwwroot/blog/
protected/views/layouts/column1.php.
All other users are guest users who can perform the following actions:
• Read posts
• Create comments
• The homepage of the system should display a list of the most recent posts.
• The system should show a cloud of tags indicating their use frequencies.
• tbl user stores the user information, including username and password.
• tbl post stores the blog post information. It mainly consists of the following columns:
• tbl comment stores the post comment information. Each comment is associated with
a post and mainly consists of the following columns:
• tbl tag stores post tag frequency information that is needed to implement the tag
cloud feature. The table mainly contains the following columns:
• tbl lookup stores generic lookup information. It is essentially a map between integer
values and text strings. The former is the data representation in our code, while the
latter is the corresponding presentation to end users. For example, we use integer
1 to represent the draft post status and string Draft to display this status to end
users. This table mainly contains the following columns:
6 1. Getting Started
– name: the textual representation of the data item that is to be displayed to end
users;
– code: the integer representation of the data item;
– type: the type of the data item;
– position: the relative display order of the data item among other items of the
same type.
The following entity-relation (ER) diagram shows the table structure and relationships
about the above tables.
Info: We name all our table names and column names in lower case. This is because
different DBMS often have different case-sensitivity treatment and we want to avoid
troubles like this.
We also prefix all our tables with tbl . This serves for two purposes. First, the prefix
introduces a namespace to these tables in case when they need to coexist with other
tables in the same database, which often happens in a shared hosting environment
where a single database is being used by multiple applications. Second, using table
prefix reduces the possibility of having some table names that are reserved keywords
in DBMS.
1.4 Overall Design 7
We divide the development of our blog application into the following milestones.
• Milestone 4: implementing portlets. It includes user menu, login, tag cloud and
recent comments portlets.
Initial Prototyping
We choose to create a SQLite database. Because the database support in Yii is built on top
of PDO, we can easily switch to use a different type of DBMS (e.g. MySQL, PostgreSQL)
without the need to change our application code.
Tip: To execute SQL statements, we may use the sqlite3 command line tool that
can be found in the SQLite official website.
To use the blog database in the skeleton application we created, we need to modify its
application configuration which is stored in the PHP script /wwwroot/blog/protected/
config/main.php. The script returns an associative array consisting of name-value pairs,
each of which is used to initialize a writable property of the application instance.
return array(
......
’components’=>array(
......
’db’=>array(
’connectionString’=>’sqlite:/wwwroot/blog/protected/data/blog.db’,
’tablePrefix’=>’tbl ’,
),
),
......
);
The above configuration says that we have a db application component whose connectionString
property should be initialized as sqlite:/wwwroot/blog/protected/data/blog.db and whose
tablePrefix property should be tbl .
With this configuration, we can access the DB connection object using Yii::app()->db
at any place in our code. Note that Yii::app() returns the application instance that we
create in the entry script. If you are interested in possible methods and properties that the
DB connection has, you may refer to its class reference. However, in most cases we are not
going to use this DB connection directly. Instead, we will use the so-called ActiveRecord
to access the database.
We would like to explain a bit more about the tablePrefix property that we set in the
configuration. This tells the db connection that it should respect the fact we are using
tbl as the prefix to our database table names. In particular, if in a SQL statement there
is a token enclosed within double curly brackets (e.g. {{post}}, then the db connection
should translate it into a name with the table prefix (e.g. tbl post) before sending it to
DBMS for execution. This feature is especially useful if in future we need to modify the
table name prefix without touching our source code. For example, if we are developing a
generic content management system (CMS), we may exploit this feature so that when it
is being installed in a new environment, we can allow users to choose a table prefix they
like.
2.2 Scaffolding 11
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
configuration as follows,
return array(
......
’components’=>array(
......
’db’=>array(
’connectionString’ => ’mysql:host=localhost;dbname=blog’,
’emulatePrepare’ => true,
’username’ => ’root’,
’password’ => ’’,
’charset’ => ’utf8’,
’tablePrefix’ => ’tbl ’,
),
),
......
);
2.2 Scaffolding
Create, read, update and delete (CRUD) are the four basic operations of data objects in an
application. Because the task of implementing the CRUD operations is so common when
developing Web applications, Yii provides a tool to automate this process (also known as
scaffolding). In this section, we will describe how to use the tool to implement CRUD for
posts and comments.
Info: Some PHP installations may use a different php.ini file for command line
(CLI) PHP parser. As a result, when running the above yiic commands, you
may encounter errors like ”YiiBase::include(PDO.php): failed to open stream...”
or ”...could not find driver”. Please double check your CLI PHP configuration by
executing the following command:
php -i
The result of the above command will show which php.ini file is being used and
which extensions are loaded. If a wrong php.ini file is used, you may use the
following command to explicitly specify the correct php.ini to use:
The commands above accomplish two tasks. First, the model command generates a model
class file for each table in the blog database. Second, the crud commands generate the
code needed by the CRUD operations for the Post and Comment models.
Tip: The command model * generates a model class for every table in the database.
Sometimes, we probably don’t want to do so (e.g. the database contains some irrel-
evant tables). In this case, we can create model classes one by one. For example,
to create the User model, we can use the command model User. The model com-
mand also has some more advanced usages. For more details, execute help model
command.
http://www.example.com/blog/index.php?r=post
http://www.example.com/blog/index.php?r=comment
Notice that the post and comment features implemented by the generated code are com-
pletely independent of each other. Also, when creating a new post or comment, we are
required to enter information, such as author id and create time, which in real applica-
tion should be set by the program. Don’t worry. We will fix these problems in the next
milestones. For now, we should be fairly satisfied as this prototype already contains most
features that we need to implement for the blog application.
To prepare for the next milestones, let’s take a closer look at the files generated by the
above commands. All the files are generated under /wwwroot/blog/protected. For conve-
nience, we group them into model files, controller files and view files:
2.2 Scaffolding 13
• model files:
– models/User.php contains the User class that extends from CActiveRecord and
can be used to access the tbl user database table;
– models/Post.php contains the Post class that extends from CActiveRecord and
can be used to access the tbl post database table;
– models/Tag.php contains the Tag class that extends from CActiveRecord and
can be used to access the tbl tag database table;
– models/Comment.php contains the Comment class that extends from CActiveRe-
cord and can be used to access the tbl comment database table;
– models/Lookup.php contains the Lookup class that extends from CActiveRecord
and can be used to access the tbl lookup database table;
• controller file:
• view files:
In order to understand better how the above files are used, we show in the following the
workflow that occurs in the blog application when displaying a list of posts:
2. The entry script is executed by the Web server which creates and initializes an
application instance to handle the request;
4. The PostController instance executes the index action by calling its actionIndex()
method. Note that index is the default action if the user does not specify an action
to execute in the URL;
5. The actionIndex() method queries database to bring back the list of recent posts;
6. The actionIndex() method renders the index view with the post data.
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.
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.
2.3 Authenticating User 15
<?php
class UserIdentity extends CUserIdentity
{
private $ id;
In the authenticate() method, we use the User class to look for a row in the User table
whose username column is the same as the given username in a case-insensitive manner.
Remember that the User class was created using the yiic tool in the prior section. Because
the User class extends from CActiveRecord, we can exploit the ActiveRecord feature to
access the User table in an OOP fashion.
In order to check if the user has entered a valid password, we invoke the validatePassword
method of the User class. We need to modify the file /wwwroot/blog/protected/models/
User.php as follows. Note that instead of storing the plain password in the database, we
store the hash result of the password and a randomly generated salt key. When validating
the user-entered password, we should compare the hash results, instead.
In the UserIdentity class, we also override the getId() method which returns the id value
of the user found in the User table. The parent implementation would return the username,
instead. Both the username and id properties will be stored in the user session and may
be accessed via Yii::app()->user from anywhere in our code.
Tip: In the UserIdentity class, we reference the class CUserIdentity without ex-
plicitly including the corresponding class file. This is because CUserIdentity is a
core class provided by the Yii framework. Yii will automatically include the class
file for any core class when it is referenced for the first time.
We also do the same with the User class. This is because the User class file is placed
under the directory /wwwroot/blog/protected/models which has been added to
the PHP include path according to the following lines found in the application
configuration:
return array(
......
’import’=>array(
’application.models.*’,
’application.components.*’,
),
......
);
The above configuration says that any class whose class file is located under either /
wwwroot/blog/protected/models or /wwwroot/blog/protected/components will
be automatically included when the class is referenced for the first time.
The UserIdentity class is mainly used by the LoginForm class to authenticate a user based
on the username and password input collected from the login page. The following code
fragment shows how UserIdentity is used:
$identity=new UserIdentity($username,$password);
2.4 Summary 17
$identity->authenticate();
switch($identity->errorCode)
{
case UserIdentity::ERROR NONE:
Yii::app()->user->login($identity);
break;
......
}
Info: People often get confused about identity and the user application component.
The former represents a way of performing authentication, while the latter is used
to represent the information related with the current user. An application can only
have one user component, but it can have one or several identity classes, depending
on what kind of authentication it supports. Once authenticated, an identity instance
may pass its state information to the user component so that they are globally
accessible via user.
To test the modified UserIdentity class, we can browse the URL http://www.example.
com/blog/index.php and try logging in with the username and password that we store in
the User table. If we use the database provided by the blog demo, we should be able to
login with username demo and password demo. Note that this blog system does not provide
the user management feature. As a result, a user cannot change his account or create a
new one through the Web interface. The user management feature may be considered as
a future enhancement to the blog application.
2.4 Summary
We have completed the milestone 1. Let’s summarize what we have done so far:
6. We generated the code that implements the basic CRUD operations for both posts
and comments;
7. We modified the authentication method to check against the tbl user table.
18 2. Initial Prototyping
For a new project, most of the time will be spent in step 1 and 4 for this first milestone.
Although the code generated by the yiic tool implements fully functional CRUD oper-
ations for a database table, it often needs to be modified in practical applications. For
this reason, in the next two milestone, our job is to customize the generated CRUD code
about posts and comments so that it reaches our initial requirements.
In general, we first modify the model class file by adding appropriate validation rules and
declaring relational objects. We then modify the controller action and view code for each
individual CRUD operation.
Chapter 3
Post Management
• the rules() method: specifies the validation rules for the model attributes;
Info: A model consists of a list of attributes, each associated with a column in the
corresponding database table. Attributes can be declared explicitly as class member
variables or implicitly without any declaration.
We first specify the validation rules which ensure the attribute values entered by users
are correct before they are saved to the database. For example, the status attribute of
Post should be an integer 1, 2 or 3. The yiic tool also generates validation rules for each
model. However, these rules are based on the table column information and may not be
appropriate.
array(’tags’, ’normalizeTags’),
In the above, we specify that the title, content and status attributes are required; the
length of title should not exceed 128; the status attribute value should be 1 (draft), 2
(published) or 3 (archived); and the tags attribute should only contain word characters
and commas. In addition, we use normalizeTags to normalize the user-entered tags so
that the tags are unique and properly separated with commas. The last rule is used by
the search feature, which we will describe later.
The validators such as required, length, in and match are all built-in validators provided
by Yii. The normalizeTags validator is a method-based validator that we need to define
in the Post class. For more information about how to specify validation rules, please refer
to the Guide.
The rules declared in this method are executed one by one when we call the validate() or
save() method of the model instance.
After making these changes, we can visit the post creation page again to verify that the
new validation rules are taking effect.
Lastly we customize the relations() method to specify the related objects of a post. By
declaring these related objects in relations(), we can exploit the powerful Relational
ActiveRecord (RAR) feature to access the related object information of a post, such as
its author and comments, without the need to write complex SQL JOIN statements.
3.1 Customizing Post Model 21
• A post belongs to an author whose class is User and the relationship is established
based on the author id attribute value of the post;
• A post has many comments whose class is Comment and the relationship is established
based on the post id attribute value of the comments. These comments should be
sorted according to their creation time and the comments must be approved.
With the above relation declaration, we can easily access the author and comments of a
post like the following:
$author=$post->author;
echo $author->username;
$comments=$post->comments;
foreach($comments as $comment)
echo $comment->content;
For more details about how to declare and use relations, please refer to the Guide.
A post is a content that is associated with a unique URL for viewing it. Instead of calling
CWebApplication::createUrl everywhere in our code to get this URL, we may add a url
22 3. Post Management
property in the Post model so that the same piece of URL creation code can be reused.
Later when we describe how beautify URLs, we will see adding this property will bring us
great convenience.
To add the url property, we modify the Post class by adding a getter method like the
following:
Note that in addition to the post ID, we also add the post title as a GET parameter in the
URL. This is mainly for search engine optimization (SEO) purpose, as we will describe in
.
Because CComponent is the ultimate ancestor class of Post, adding the getter method
getUrl() enables us to use the expression like $post->url. When we access $post->url,
the getter method will be executed and its result is returned as the expression value. For
more details about such component features, please refer to the guide.
Because the status of a post is stored as an integer in the database, we need to provide a
textual representation so that it is more intuitive when being displayed to end users. In a
large system, the similar requirement is very common.
As a generic solution, we use the tbl lookup table to store the mapping between integer
values and textual representations that are needed by other data objects. We modify the
Lookup model class as follows to more easily access the textual data in the table,
self::loadItems($type);
return self::$ items[$type];
}
Our new code mainly provides two static methods: Lookup::items() and Lookup::item().
The former returns a list of strings belonging to the specified data type, while the latter
returns a particular string for the given data type and data value.
Our blog database is pre-populated with two lookup types: PostStatus and CommentStatus.
The former refers to the possible post statuses, while the latter the comment statuses.
In order to make our code easier to read, we also declare a set of constants to represent
the status integer values. We should use these constants through our code when referring
to the corresponding status values.
Therefore, we can call Lookup::items(’PostStatus’) to get the list of possible post statuses
(text strings indexed by the corresponding integer values), and call Lookup::item(’PostStatus’,
Post::STATUS PUBLISHED) to get the string representation of the published status.
24 3. Post Management
The first thing we want to do is to customize the access control because the code generated
by yiic does not fit our needs.
The above rules state that all users can access the index and view actions, and authenti-
cated users can access any actions, including the admin action. The user should be denied
access in any other scenario. Note that these rules are evaluated in the order they are
listed here. The first rule matching the current context makes the access decision. For
example, if the current user is the system owner who tries to visit the post creation page,
the second rule will match and it will give the access to the user.
The create and update operations are very similar. They both need to display an HTML
form to collect user inputs, validate them, and save them into database. The main differ-
ence is that the update operation will pre-populate the form with the existing post data
found in the database. For this reason, the yiic tool generates a partial view /wwwroot/
3.2 Creating and Updating Posts 25
We first change the form.php file so that the HTML form only collects the inputs we
want: title, content, tags and status. We use plain text fields to collect inputs for the
first three attributes, and a dropdown list to collect input for status. The dropdown list
options are the text displays of the possible post statuses:
In the implementation, because we want to detect if the user changes the tags in case he is
updating an existing post, we need to know what the old tags are. For this reason, we also
write the afterFind() method to keep the old tags in the variable oldTags. The method
afterFind() is invoked automatically by Yii when an AR record is populated with the
data from database.
We are not going to give details of the Tag::updateFrequency() method here. Interested
readers may refer to the file /wwwroot/yii/demos/blog/protected/models/Tag.php.
private $ model;
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;
}
Our change mainly lies in the loadModel() method. In this method, we query the Post
table according to the id GET parameter. If the post is not found or if it is not published
or archived (when the user is a guest), we will throw a 404 HTTP error. Otherwise the
post object is returned to actionView() which in turn passes the post object to the view
script for further display.
The change in the view script is mainly about ajdusting the formatting and styles of the
post display. We will not go into details here. Interested readers may refer to /wwwroot/
blog/protected/views/post/view.php.
Like the view operation, we customize the index operation in two places: the actionIndex()
method in PostController and the view file /wwwroot/blog/protected/views/post/index.
php. We mainly need to add the support for displaying a list of posts that are associated
with a specified tag.
$criteria=new CDbCriteria(array(
’condition’=>’status=’.Post::STATUS PUBLISHED,
’order’=>’update time DESC’,
’with’=>’commentCount’,
));
if(isset($ GET[’tag’]))
$criteria->addSearchCondition(’tags’,$ GET[’tag’]);
$this->render(’index’,array(
’dataProvider’=>$dataProvider,
));
}
In the above, we first create a query criteria for retrieving post list. The criteria states
that only published posts should be returned and they should be sorted according to their
update time in descending order. Because when displaying a post in the list, we want to
show how many comments the post has received, in the criteria we also specify to bring
back commentCount, which if you remember, is a relation declared in Post::relations().
In case when a user wants to see posts with a specific tag, we would add a search condition
to the criteria to look for the specified tag.
Using the query criteria, we create a data provider, which mainly serves for three purposes.
First, it does pagination of the data when too many results may be returned. Here
we customize the pagination by setting the page size to be an application parameter
postsPerPage. Second, it does sorting according to the user request. And finally, it feeds
the paginated and sorted data to widgets or view code for presentation.
After we finish with actionIndex(), we modify the index view as follows. Our change is
mainly about adding the h1 header when the user specifies to display posts with a tag.
)); ?>
Note that in the above, we use CListView to display the post list. This widget requires a
partial view to display the detail of each individual post. Here we specify the partial view
to be view, which means the file /wwwroot/blog/protected/views/post/ view.php. In this
view script, we can acccess the post instance being displayed via a local variable named
$data.
The admin operation shows posts with all statuses in a tabular view. The view supports
sorting and pagination. The following is the actionAdmin() method in PostController:
The above code is generated by the yiic tool without any modification. It first creates
a Post model under the search scenario. We will use this model to collect the search
conditions that the user specifies. We then assign to the model the user-suppiled data, if
any. Finally, we render the admin view with the model.
<?php
$this->breadcrumbs=array(
’Manage Posts’,
);
30 3. Post Management
?>
<h1>Manage Posts</h1>
We use CGridView to display the posts. It allows us to sort by a column and paginate
through the posts if there are too many to be displayed in a single page. Our change is
mainly about how to display each column. For example, for the title column, we specify
that it should be displayed as a hyperlink that points to the detailed view of the post.
The expression $data->url returns the value of the url property that we define in the Post
class.
In the admin data grid, there is a delete button in each row. Clicking on the button should
delete the corresponding post. Internally, this triggers the delete action implemented as
follows:
{
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.’);
}
The above code is the one generated by the yiic tool without any change. We would like
to explain a little bit more about the checking on $ POST[’ajax’]. The CGridView widget
has a very nice feature that its sorting, pagination and deletion operations are all done
in AJAX mode by default. That means, the whole page does not get reloaded if any of
the above operations is performed. However, it is also possible that the widget runs in
non-AJAX mode (by setting its ajaxUpdate property to be false or disabling JavaScript on
the client side). It is necessary for the delete action to differentiate these two scenarios: if
the delete request is made via AJAX, we should not redirect the user browser; otherwise,
we should.
Deleting a post should also cause the deletion of all comments for that post. In addition,
we should also update the tbl tag table regarding the tags for the deleted post. Both of
these tasks can be achieved by writing an afterDelete method in the Post model class as
follows,
The above code is very straightforward: it first deletes all those comments whose post id
is the same as the ID of the deleted post; it then updates the tbl tag table for the tags
of the deleted post.
32 3. Post Management
Tip: We have to explicitly delete all comments for the deleted post here because
SQLite does not really support foreign key constraints. In a DBMS that supports
this constraint (such as MySQL, PostgreSQL), the foreign key constraint can be set
up such that the DBMS automatically deletes the related comments if the post is
deleted. In that case, we no longer this explicit deletion call in our code.
Chapter 4
Comment Management
We first customize the validation rules generated by the yiic tool. The following rules are
used for comments:
In the above, we specify that the author, email and content attributes are required; the
length of author, email and url cannot exceed 128; the email attribute must be a valid
email address; and the url attribute must be a valid URL.
We then customize the attributeLabels() method to declare the label display for each
model attribute. This method returns an array consisting of name-label pairs. When we
call CHtml::activeLabel() to display an attribute label.
34 4. Comment Management
Because we want to record the creation time of a comment, we override the beforeSave()
method of Comment like we do for the Post model:
In order to enhance the user interactivity, we would like to prompt users the possible
errors each time he finishes entering one field. This is known client-side input validation.
We will show how this can be done in Yii seamlessly and extremely easy. Note that this
requires Yii version 1.1.1 or later.
4.2 Creating and Displaying Comments 35
Instead of displaying and creating comments on individual pages, we use the post detail
page (generated by the view action of PostController). Below the post content display,
we display first a list of comments belonging to that post and then a comment creation
form.
In order to display comments on the post detail page, we modify the view script /wwwroot/
blog/protected/views/post/view.php as follows,
<div id="comments">
<?php if($model->commentCount>=1): ?>
<h3>
<?php echo $model->commentCount . ’comment(s)’; ?>
</h3>
In the above, we call renderPartial() to render a partial view named comments to display
the list of comments belonging to the current post. Note that in the view we use the
expression $model->comments to retrieve the comments for the post. This is valid because
we have declared a comments relation in the Post class. Evaluating this expression would
trigger an implicit JOIN database query to bring back the proper comments. This feature
is known as lazy relational query.
The partial view comments is not very interesting. It mainly goes through every comment
and displays the detail of it. Interested readers may refer to /wwwroot/yii/demos/blog/
protected/post/ comments.php.
$comment=$this->newComment($post);
$this->render(’view’,array(
’model’=>$post,
’comment’=>$comment,
));
}
In the above, we call the newComment() method before we render view. In the newComment()
method, we generate a Comment instance and check if the comment form is submitted. If so,
we try to add the comment for the post by calling $post->addComment($comment). If it goes
through, we refresh the post detail page. In case the comment needs to be approved, we will
show a flash message to indicate this decision. A flash message is usually a confirmation
message displayed to end users. If the user clicks on the refresh button of his browser, the
message will disappear.
......
<div id="comments">
......
<h3>Leave a Comment</h3>
In the above code, we display the flash message if it is available. If not, we display
the comment input form by rendering the partial view /wwwroot/blog/protected/views/
comment/ form.php.
In order to support client-side validation of the comment form, we need to make some
minor changes to both the comment form view /wwwroot/blog/protected/views/comment/
form.php and the newComment() method.
<div class="form">
And in the newComment() method, we insert a piece of code to respond to the AJAX
validation requests. The code checks if there is a POST variable named ajax. If so, it
displays the validation results by calling CActiveForm::validate.
if(isset($ POST[’Comment’]))
{
38 4. Comment Management
$comment->attributes=$ POST[’Comment’];
if($post->addComment($comment))
{
if($comment->status==Comment::STATUS PENDING)
Yii::app()->user->setFlash(’commentSubmitted’,’Thank you...’);
$this->refresh();
}
}
return $comment;
}
The code generated by yiic for updating and deleting comments remains largely un-
changed.
When comments are newly created, they are in pending approval status and need to be
approved in order to be visible to guest users. Approving a comment is mainly about
changing the status column of the comment.
In the above, when the approve action is invoked via a POST request, we call the approve()
method defined in the Comment model to change the status. We then redirect the user
browser to the page displaying the post that this comment belongs to.
4.3 Managing Comments 39
We also modify the actionIndex() method of Comment to show all comments. We would
like to see comments pending approved to show up first.
$this->render(’index’,array(
’dataProvider’=>$dataProvider,
));
}
Notice that in the above code, because both tbl post and tbl comment have columns status
and create time, we need to disambiguate the corresponding column reference by prefixing
them with table alias names. As described in the guide, the alias for the primary table in
a relational query is always t. Therefore, we are prefixing t to the status and create time
columns in the above code.
Like the post index view, the index view for CommentController uses CListView to display
the comment list which in turn uses the partial view /wwwroot/blog/protected/views/
comment/ view.php to display the detail of each individual comment. We will not go into
details here. Interested readers may refer to the corresponding file in the blog demo
/wwwroot/yii/demos/blog/protected/views/comment/ view.php.
40 4. Comment Management
Chapter 5
Portlets
In this section, we will develop our first concrete portlet - the user menu portlet which
displays a list of menu items that are only available to authenticated users. The menu
contains four items:
• Create New Post: a hyperlink that leads to the post creation page;
• Logout: a link button that would log out the current user.
We create the UserMenu class to represent the logic part of the user menu portlet. The
class is saved in the file /wwwroot/blog/protected/components/UserMenu.php which has the
following content:
Yii::import(’zii.widgets.CPortlet’);
The UserMenu class extends from the CPortlet class from the zii library. It overrides
both the init() method and the renderContent() method of CPortlet. The former sets
the portlet title to be the name of the current user; the latter generates the portlet body
content by rendering a view named userMenu.
Tip: Notice that we have to explicitly include the CPortlet class by calling Yii::
import() before we refer to it the first time. This is because CPortlet is part of the
zii project – the official extension library for Yii. For performance consideration,
classes in this project are not listed as core classes. Therefore, we have to import it
before we use it the first time.
Next, we create the userMenu view which is saved in the file /wwwroot/blog/protected/
components/views/userMenu.php:
<ul>
<li><?php echo CHtml::link(’Create New Post’,array(’post/create’)); ?></li>
<li><?php echo CHtml::link(’Manage Posts’,array(’post/admin’)); ?></li>
<li><?php echo CHtml::link(’Approve Comments’,array(’comment/index’))
. ’ (’ . Comment::model()->pendingCommentCount . ’)’; ?></li>
<li><?php echo CHtml::link(’Logout’,array(’site/logout’)); ?></li>
</ul>
Info: By default, view files for a widget should be placed under the views sub-
directory of the directory containing the widget class file. The file name must be
the same as the view name.
It is time for us to make use of our newly completed UserMenu portlet. We modify the
layout view file /wwwroot/blog/protected/views/layouts/column2.php as follows:
5.2 Creating Tag Cloud Portlet 43
......
<div id="sidebar">
<?php if(!Yii::app()->user->isGuest) $this->widget(’UserMenu’); ?>
</div>
......
In the above, we call the widget() method to generate and execute an instance of the
UserMenu class. Because the portlet should only be displayed to authenticated users, we
only call widget() when the isGuest property of the current user is false (meaning the
user is authenticated).
2. Click on the Login hyperlink and fill out the login form to login. If successful, verify
that the UserMenu portlet appears in the side bar and the portlet has the username
as its title.
3. Click on the ’Logout’ hyperlink in the UserMenu portlet. Verify that the logout action
is successful and the UserMenu portlet disappears.
5.1.5 Summary
What we have created is a portlet that is highly reusable. We can easily reuse it in a
different project with little or no modification. Moreover, the design of this portlet follows
closely the philosophy that logic and presentation should be separated. While we did not
point this out in the previous sections, such practice is used nearly everywhere in a typical
Yii application.
Yii::import(’zii.widgets.CPortlet’);
foreach($tags as $tag=>$weight)
{
$link=CHtml::link(CHtml::encode($tag), array(’post/index’,’tag’=>$tag));
echo CHtml::tag(’span’, array(
’class’=>’tag’,
’style’=>"font-size:{$weight}pt",
), $link)."\n";
}
}
}
Unlike the UserMenu portlet, the TagCloud portlet does not use a view. Instead, its presen-
tation is done in the renderContent() method. This is because the presentation does not
contain much HTML tags.
We display each tag as a hyperlink to the post index page with the corresponding tag
parameter. The font size of each tag link is adjusted according to their relative weight
among other tags. If a tag has higher frequency value than the other, it will have a bigger
font size.
Usage of the TagCloud portlet is very simple. We modify the layout file /wwwroot/blog/
protected/views/layouts/column2.php as follows,
......
<div id="sidebar">
</div>
5.3 Creating Recent Comments Portlet 45
......
Yii::import(’zii.widgets.CPortlet’);
In the above we invoke the findRecentComments method which is defined in the Comment
class as follows,
......
<div id="sidebar">
</div>
......
Chapter 6
Final Work
/index.php?r=post/show&id=1&title=A+Test+Post
In this section, we describe how to beautify these URLs and make them SEO-friendly.
Our goal is to be able to use the following URLs in the application:
1. /index.php/posts/yii: leads to the page showing a list of posts with tag yii;
3. /index.php/post/update?id=1: leads to the page that allows updating the post with
ID 1.
Note that in the second URL format, we include the post title in the URL. This is mainly
to make the URL SEO friendly. It is said that search engines may also respect the words
found in a URL when it is being indexed.
return array(
......
’components’=>array(
......
’urlManager’=>array(
’urlFormat’=>’path’,
’rules’=>array(
48 6. Final Work
’post/<id:\d+>/<title:.*?>’=>’post/view’,
’posts/<tag:.*?>’=>’post/index’,
’<controller:\w+>/<action:\w+>’=>’<controller>/<action>’,
),
),
),
);
In the above, we configure the urlManager component by setting its urlFormat property
to be path and adding a set of rules.
The rules are used by urlManager to parse and create the URLs in the desired format.
For example, the second rule says that if a URL /index.php/posts/yii is requested, the
urlManager component should be responsible to dispatch the request to the route post/
index and generate a tag GET parameter with the value yii. On the other hand, when
creating a URL with the route post/index and parameter tag, the urlManager component
will also use this rule to generate the desired URL /index.php/posts/yii. For this reason,
we say that urlManager is a two-way URL manager.
The urlManager component can further beautify our URLs, such as hiding index.php in
the URLs, appending suffix like .html to the URLs. We can obtain these features easily
by configuring various properties of urlManager in the application configuration. For more
details, please refer to the Guide.
return array(
’preload’=>array(’log’),
......
’components’=>array(
’log’=>array(
’class’=>’CLogRouter’,
’routes’=>array(
array(
’class’=>’CFileLogRoute’,
6.3 Final Tune-up and Deployment 49
’levels’=>’error, warning’,
),
),
),
......
),
);
With the above configuration, if an error or warning occurs, detailed information will be
logged and saved in a file located under the directory /wwwroot/blog/protected/runtime.
The log component offers more advanced features, such as sending log messages to a list
of email addresses, displaying log messages in JavaScript console window, etc. For more
details, please refer to the Guide.
We change to use the post list page as the home page. We modify the application config-
uration as follows,
return array(
......
’defaultController’=>’post’,
......
);
Tip: Because PostController already declares list to be its default action, when
we access the home page of the application, we will see the result generated by the
list action of the post controller.
Because ActiveRecord relies on the metadata about tables to determine the column infor-
mation, it takes time to read the metadata and analyze it. This may not be a problem
during development stage, but for an application running in production mode, it is a total
50 6. Final Work
waste of time if the database schema does not change. Therefore, we should enable the
schema caching by modifying the application configuration as follows,
return array(
......
’components’=>array(
......
’cache’=>array(
’class’=>’CDbCache’,
),
’db’=>array(
’class’=>’system.db.CDbConnection’,
’connectionString’=>’sqlite:/wwwroot/blog/protected/data/blog.db’,
’schemaCachingDuration’=>3600,
),
),
);
In the above, we first add a cache component which uses a default SQLite database as the
caching storage. If our server is equipped with other caching extensions, such as APC,
we could change to use them as well. We also modify the db component by setting its
schemaCachingDuration property to be 3600, which means the parsed database schema
data can remain valid in cache for 3600 seconds.
We modify the entry script file /wwwroot/blog/index.php by removing the line defining
the constant YII DEBUG. This constant is useful during development stage because it allows
Yii to display more debugging information when an error occurs. However, when the
application is running in production mode, displaying debugging information is not a
good idea because it may contain sensitive information such as where the script file is
located, and the content in the file, etc.
The final deployment process manly involves copying the directory /wwwroot/blog to the
target directory. The following checklist shows every needed step:
3. Edit the entry script file index.php by pointing the $yii variable to the new Yii
bootstrap file;
6.4 Future Enhancements 51
4. Edit the file protected/yiic.php by setting the $yiic variable to be the new Yii
yiic.php file;
5. Change the permission of the directories assets and protected/runtime so that they
are writable by the Web server process.
Without writing any code, our blog application is already themeable. To use a theme,
we mainly need to develop the theme by writing customized view files in the theme. For
example, to use a theme named classic that uses a different page layout, we would create
a layout view file /wwwroot/blog/themes/classic/views/layouts/main.php. We also need
to change the application configuration to indicate our choice of the classic theme:
return array(
......
’theme’=>’classic’,
......
);
6.4.2 Internationalization
We may also internationalize our blog application so that its pages can be displayed in
different languages. This mainly involves efforts in two aspects.
First, we may create view files in different languages. For example, for the list page
of PostController, we can create a view file /wwwroot/blog/protected/views/post/zh cn/
list.php. When the application is configured to use simplified Chinese (the language code
is zh cn), Yii will automatically use this new view file instead of the original one.
Second, we may create message translations for those messages generated by code. The
message translations should be saved as files under the directory /wwwroot/blog/protected/
messages. We also need to modify the code where we use text strings by enclosing them
in the method call Yii::t().
While the Yii framework itself is very efficient, it is not necessarily true that an application
written in Yii efficient. There are several places in our blog application that we can improve
52 6. Final Work
the performance. For example, the tag clould portlet could be one of the performance
bottlenecks because it involves complex database query and PHP logic.
We can make use of the sophisticated caching feature provided by Yii to improve the
performance. One of the most useful components in Yii is COutputCache, which caches a
fragment of page display so that the underlying code generating the fragment does not need
to be executed for every request. For example, in the layout file /wwwroot/blog/protected/
views/layouts/column2.php, we can enclose the tag cloud portlet with COutputCache:
With the above code, the tag cloud display will be served from cache instead of being
generated on-the-fly for every request. The cached content will remain valid in cache for
3600 seconds.
Our blog application only has very basic functionalities. To become a complete blog
system, more features are needed, for example, calendar portlet, email notifications, post
categorization, archived post portlet, and so on. We will leave the implementation of these
features to interested readers.