Also available in these languages:
English日本語polskiРусский简体中文

Customizing Post Model

The Post model class generated by the yiic tool mainly needs to be modified in two places:

  • the rules() method: specifies the validation rules for the model attributes;
  • the 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.

Customizing rules() Method

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.

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 as id and create_time in the Post model, which are set by our code or database, should not be in rules(). 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.

Customizing relations() Method

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.

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

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

Adding url Property

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

Representing Status in Text

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.

$Id: post.model.txt 2119 2010-05-10 01:27:29Z qiang.xue $
If you find any typos or errors in the tutorial, please create a Yii ticket to report it. If it is a translation error, please create a Yiidoc ticket, instead. Thank you.

Total 18 comments:

#195
Creating error messages for just one attribute from array
by So1 at 4:20am on April 14, 2009.

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.

#235
Hmmmm
by gearond at 4:44pm on April 27, 2009.

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.

#238
safeAttributes() Method
by jojo5544 at 4:35am on April 28, 2009.

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

#239
Thinking about this
by gearond at 4:17pm on April 28, 2009.

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.

#240
better function name
by gearond at 4:19pm on April 28, 2009.

'allowFromUserInput'

#256
getStatusOptions
by jamiem at 5:08am on May 1, 2009.

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.

#484
Blog tutorial code samples has to be updated
by aztech at 7:10am on July 21, 2009.

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}

#919
getStatusOptions
by rideon88 at 12:26pm on December 20, 2009.

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?

#1036
sighhh...
by tyuwan at 1:20am on January 23, 2010.

this tutorial dont even talk about

$this->tags=Tag::array2string(array_unique(Tag::string2array($this->tags)));

#1052
Creating error messages for just one attribute from array
by hezll at 1:24am on January 28, 2010.

array('title', 'required','message'=>'title can\'t be empty'), array('content, status', 'required','message'=>'XXX can\'t be empty'),

is ok!

#1215
About array problem
by Edigu at 3:28pm on March 1, 2010.

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.

#1346
Comment Constants
by Incubusaurus at 4:11am on April 3, 2010.

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



#1465
Create Post Failure
by CNM at 5:36pm on May 6, 2010.

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

#1521
Error in code - Undefined method
by KVron at 5:06am on May 25, 2010.

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?

#1522
Error in code - Undefined method - SOLVED.
by KVron at 5:12am on May 25, 2010.

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

#1626
In Tag class we have no such static methods as : array2string() and string2array()
by Sart at 6:06am on June 30, 2010.

Why, missprint ?

#1627
In Tag class we have no such static methods as : array2string() and string2array()
by Sart at 6:14am on June 30, 2010.

Sorry, I've found in Tag class in "framework\demos\blog\protected\models\Tag.php" ...

#1635
class Loockup - on this page...
by Sart at 7:50pm on July 1, 2010.

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


Your Comment:

You may enter comment using Markdown syntax.

Please login with your forum account.
Note: you must have at least ONE forum post with your account.