- Getting Started
- Initial Prototyping
- Post Management
- Comment Management
- Portlets
- Final Work
The Post model class generated by the yiic tool mainly needs to be modified in two places:
rules() method: specifies the validation rules for the model attributes;relations() method: specifies the related objects;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.
rules() MethodWe 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.
Based on the requirement analysis, we modify the rules() method as follows:
public function rules() { return array( array('title, content, status', 'required'), array('title', 'length', 'max'=>128), array('status', 'in', 'range'=>array(1,2,3)), array('tags', 'match', 'pattern'=>'/^[\w\s,]+$/', 'message'=>'Tags can only contain word characters.'), array('tags', 'normalizeTags'), array('title, status', 'safe', 'on'=>'search'), ); }
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.
public function normalizeTags($attribute,$params) { $this->tags=Tag::array2string(array_unique(Tag::string2array($this->tags))); }
where array2string and string2array are new methods defined in the Tag model class. Please refer to the file /wwwroot/yii/demos/blog/protected/models/Tag.php for more details.
The rules declared in the rules() method are executed one by one when we call the validate() or save() method of the model instance.
Note: It is very important to remember that attributes appearing in
rules()must be those to be entered by end users. Other attributes, such asidandcreate_timein thePostmodel, which are set by our code or database, should not be inrules(). For more details, please refer to Securing Attribute Assignments.
After making these changes, we can visit the post creation page again to verify that the new validation rules are taking effect.
relations() MethodLastly 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.
We customize the relations() method as follows:
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), ); }
We also introduce in the Comment model class two constants that are used in the above method:
class Comment extends CActiveRecord { const STATUS_PENDING=1; const STATUS_APPROVED=2; ...... }
The relations declared in relations() state that
User and the relationship is established based on the author_id attribute value of the post;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.commentCount relation is a bit special as it returns back an aggregation result which is about how many comments the post has.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.
url PropertyA 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 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:
class Post extends CActiveRecord { public function getUrl() { return Yii::app()->createUrl('post/view', array( 'id'=>$this->id, 'title'=>$this->title, )); } }
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 Beautifying URLs.
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,
class Lookup extends CActiveRecord { private static $_items=array(); public static function items($type) { if(!isset(self::$_items[$type])) self::loadItems($type); return self::$_items[$type]; } public static function item($type,$code) { if(!isset(self::$_items[$type])) self::loadItems($type); return isset(self::$_items[$type][$code]) ? self::$_items[$type][$code] : false; } private static function loadItems($type) { self::$_items[$type]=array(); $models=self::model()->findAll(array( 'condition'=>'type=:type', 'params'=>array(':type'=>$type), 'order'=>'position', )); foreach($models as $model) self::$_items[$type][$model->code]=$model->name; } }
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.
class Post extends CActiveRecord { const STATUS_DRAFT=1; const STATUS_PUBLISHED=2; const STATUS_ARCHIVED=3; ...... }
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.
I think most of this is in any good database. SQLite???? Don't know. But Oracle, Informix, Posgres, SQLServer- they do all of this. Now, the messages may not be able to be translated so easily coming from the database, and no matter the database chosen, you will have control over the messages.
I just read an interesting article by a Leading Developer @ Ebay. They actually do some of what this page is about. Or at least, they offload the database functionality onto code closer to the customer, i.e. the interpreter or a library. But, they do it on stuff that they are not worried about losing 'semi quoted'. What's above won't lose anything, but even though it's a consistent, comprehesive, admirably designed system, I would put it into the data base, probabaly.
I do feel that with an OOP approach, MAYBE it makes more sense above. OTOH, it should also be done in the datebase, so then it's being done twice.
In 'Customizing safeAttributes() Method' this method isn't created by yiic shell command?
does it just need $post->attributes=$_POST'Post'; this one line?
thanks
I am beginning to like this more and more, even the safeAttributes method. BUT, I will probably extend the idea as follows:
FIRST, 'safe' implies free from maliciousness, i.e., filtered. a better name would be 'userDefineableAttributes'.
Second, I am thinking of extending yiic to also work off of a XML definition file, so that not only are the rules for 'userDefineableAttributes' automatically generated, but so are the SQL files for creating the database tables.
This means in XML, using a free good editor like XMLMarker, define all the parameters of creating the M,the M backend, the V, and the C.
Can you include in this tutorial how to dynamically retrieve the statuses from a db table as well as from constants. The tutorial needs an example of how to create lookup fields/lists to be used in forms, setting the selected index of the current status.
E.g.
Below is current code for relations in Post table
{code}
public function relations()
{
return array(
'author'=>array(self::BELONGS_TO, 'User', 'authorId'),
'comments'=>array(self::HAS_MANY, 'Comment', 'postId', 'order'=>'comments.createTime'),
'tagFilter'=>array(self::MANY_MANY, 'Tag', 'PostTag(postId, tagId)',
'joinType'=>'INNER JOIN',
'condition'=>'tagFilter.name=:tag'),
);
}
{code}
For this block of code
public function getStatusText() { $options = $this->statusOptions; return isset($options$this->status) ? $options$this->status : "unknown ({$this->status})"; }
shouldn't $options = $this->getStatusOptions()
?? where does $this->statusOptions come from?
this tutorial dont even talk about
$this->tags=Tag::array2string(array_unique(Tag::string2array($this->tags)));
array('title', 'required','message'=>'title can\'t be empty'), array('content, status', 'required','message'=>'XXX can\'t be empty'),
is ok!
You can by-pass the array uniqe step in normalizeTags if you have problems with try to run example.
Because in this example, normalizeTags() method uses array2string() static method in Tag class that not we have.
public function normalizeTags($attribute,$params) { //$this->tags=Tag::array2string(array_unique(Tag::string2array($this->tags))); $this->tags = $this->tags; }
after this, example sholud be work.
This section needs to mention that the Comment class requires two constants to be defined:
const STATUS_PENDING=1; const STATUS_APPROVED=2;
These can be found in the sample demo code:
/wwwroot/yii/demos/blog/protected/models/Comment.php
When you try to connect the "Create Post" screen, if you get an unformatted screen and the following message:
"Fatal error: Cannot instantiate abstract class CActiveRecord in ..."
Add the following code to your modified "Lookup" class:
/** * Returns the static model of the specified AR class. * @return CActiveRecord the static model class */ public static function model($className=CLASS) { return parent::model($className); }
/** * @return string the associated database table name */ public function tableName() { return '{{Lookup}}'; }
The following: $this->tags=Tag::array2string(array_unique(Tag::string2array($this->tags)));
will cause this:
Fatal error: Call to undefined method Tag::array2string() in C:\xampp\htdocs\blog\protected\models\Post.php on line 53
Will the function "array2string" be introduced in a newer version? or it's just a tutorial error?
Found the missing function in the "framework\demos\blog" directory:
public static function string2array($tags)
{
return preg_split('/\s*,\s*/',trim($tags),-1,PREG_SPLIT_NO_EMPTY);
}
Why, missprint ?
Sorry, I've found in Tag class in "framework\demos\blog\protected\models\Tag.php" ...
Functions occasionally sriped: public static function model($className=CLASS) { return parent::model($className); }
/**
* @return string the associated database table name
*/
public function tableName()
{
return '{{lookup}}';
}
As it were described above, we can determine rules to check for each attribute of the model:
return array( array('title, content, status', 'required'), array('tags', 'match', 'pattern'=>'/^\w\s,+$/', 'message'=>'Tags can only contain word characters.'), );
for tags we also determined an error message to show our user, but what if i want to specify error message for only 'title' if user left it empty? In this case i must type exactly 1 code row for each error type and for each attribute. Its just my wish for developers of Yii framework make it more simple by using message either like a string (for all attributes) and as array, like this: array('title, content, status', 'required', 'message'=>array('title'=>'Please, enter a title', '*'=>'Requred filds cant be empty')), In this case, if user forgot type content, he'll get 'Requred filds cant be empty' message, but if he forgot enter title, he'll get 'Please, enter a title'.
Thank u for consideration. Sorry for my english.