Customizing Post Model

The Post model class generated by the Gii 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.

1. 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 Gii 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 we need to define in the Tag model class:

public static function string2array($tags)
{
    return preg_split('/\s*,\s*/',trim($tags),-1,PREG_SPLIT_NO_EMPTY);
}
 
public static function array2string($tags)
{
    return implode(', ',$tags);
}

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.

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

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

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

Total 7 comments

#15516 report it
mi.sarah at 2013/11/18 05:52am
set time on creating new posts

First, you need to add the beforeSave() method in post.php-Model. It's an addition to Comment #4625 from Revelis Luc Bonnin. To set the date and time add to the beforeSave() method as followed:

protected function beforeSave() {
    ...
        if($this->isNewRecord) {
            # set time on creating posts
            $this->create_time=$this->update_time=time();
            ...
        }
        else
            # changes time at updating the post
            $this->update_time=time();
        return true;
    }
    ...
}
#8956 report it
nomeacuerdo at 2012/07/09 03:38pm
Lookup Class

Don't forget to define the tableName in the Lookup controller:

public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function tableName()
    {
        return '{{lookup}}';
    }
#8951 report it
amitabh at 2012/07/09 10:34am
had to modify the first line of rules in the posts model to get it working

Had to change the first line of the rules to add the author_id too,

array('title, content, status, author_id', 'required'),

else it was showing "foreign key constraints error"

CDbCommand failed to execute the SQL statement: SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`yiiblog`.`tbl_post`, CONSTRAINT `FK_post_author` FOREIGN KEY (`author_id`) REFERENCES `tbl_user` (`id`) ON DELETE CASCADE). The SQL statement executed was: INSERT INTO `tbl_post` (`title`, `content`, `tags`, `status`) VALUES (:yp0, :yp1, :yp2, :yp3)

Also see comment #4625 by Revelis towards the bottom

#7147 report it
Dan O'Donnell at 2012/02/27 09:01pm
important point about relations

After explaining the relations() method, the tutorial notes...

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

It is worthwhile reading this closely. The point is that the relations() method makes the attributes of the related tables into attributes of the referring class. That is to say that the attributes of the author table are now available inside post in the same way the original attributes of post were. I.e. $post->post_id and $post->author->username. Although username is not mentioned anywhere in the Post class, it is inherited from Author through the relations() expression.

#7111 report it
Dan O'Donnell at 2012/02/24 01:59am
representing status code is to be /added to/ the Lookup model

If you are having trouble in the next section with the dropdown list, then the issue may be here in the Lookup model. The suggested code above is to be added to the lookup model, it isn't supposed to replace it. Elsewhere in this tutorial, the code samples have had elipses (i.e. ".....") in the code to indicate the bits you are supposed to leave and no elipsis when the code sample is to replace the existing code. But that's not the case here. You add this code to the Lookup model.

#4626 report it
Revelis Luc Bonnin at 2011/07/27 04:10am
Missing Tag::string2array() and Tag::array2string()

When using method normalizeTags() in Post model, you refer to 2 Tag model methods. Don't forget to add them in Tag model :

public static function string2array($tags)
{
    return preg_split('/\s*,\s*/',trim($tags),-1,PREG_SPLIT_NO_EMPTY);
}
 
public static function array2string($tags)
{
    return implode(', ',$tags);
}
#4625 report it
Revelis Luc Bonnin at 2011/07/27 04:08am
Create a new post won't work at this step (missing beforeSave that will be added later)

At the end of the point "1-Customizing rules() Method" , when said "After making these changes, we can visit the post creation page again to verify that the new validation rules are taking effect.", don't try to create a new entry because it won't work (FK constraint thrown). You will have to complete next step "creating and updating post" to be able to proceed to a post creation (owing to beforeSave() call and author_id instanciation).

If you want to try it anyway, simply add a beforeSave() method in Post model to indicate author_id when creating a new Post

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

Leave a comment

Please to leave your comment.