Scenario ¶
Assume we have many categories and many posts.
A post can belongs to multiple categories, and a category can have many posts. We define posts' categories using a junction table.
Now, we want to develop a page to create/update a post with a list box (or a check box list) that enables the user to define the categories of the post.
Models ¶
Here are 4 models involved in the page we want to develop.
- Post extends ActiveRecord (representing posttable)- id
- title
- body
- ... etc.
 
- Category extends ActiveRecord (representing categorytable)- id
- name
- ... etc.
 
- PostCategory extends ActiveRecord (representing post_categoryjunction table)- post_id
- category_id
 
- PostWithCategories extends Post- (all the inherited attributes of Post)
- category_ids
 
As for the first 3 models, you can create them easily with the help of Gii.
PostWithCategories model ¶
And the last one is extended from Post. It has an additional attribute called 'category_ids' that will be used to handle the categories of the post. Note that 'category_ids' attribute is an array of category ids.
class PostWithCategories extends Post
{
    /**
     * @var array IDs of the categories
     */
    $category_ids = [];
    
    /**
     * @return array the validation rules.
     */
    public function rules()
    {
        return ArrayHelper::merge(parent::rules(), [
            // each category_id must exist in category table (*1)
            ['category_ids', 'each', 'rule' => [
                    'exist', 'targetClass' => Category::className(), 'targetAttribute' => 'id'
                ]
            ],
        ]);
    }
    /**
     * @return array customized attribute labels
     */
    public function attributeLabels()
    {
        return ArrayHelper::merge(parent::attributeLabels(), [
            'category_ids' => 'Categories',
        ]);
    }
    /**
     * load the post's categories (*2)
     */
    public function loadCategories()
    {
        $this->category_ids = [];
        if (!empty($this->id)) {
            $rows = PostCategory::find()
                ->select(['category_id'])
                ->where(['post_id' => $this->id])
                ->asArray()
                ->all();
            foreach($rows as $row) {
               $this->category_ids[] = $row['category_id'];
            }
        }
    }
    /**
     * save the post's categories (*3)
     */
    public function saveCategories()
    {
        /* clear the categories of the post before saving */
        PostCategory::deleteAll(['post_id' => $this->id]);
        if (is_array($this->category_ids)) {
            foreach($this->category_ids as $category_id) {
                $pc = new PostCategory();
                $pc->post_id = $this->id;
                $pc->category_id = $category_id;
                $pc->save();
            }
        }
        /* Be careful, $this->category_ids can be empty */
    }
}
(*1) In the rules for the validation, we use EachValidator to validate the array of
category_idsattribute.(*2) loadCategories method loads the IDs of the post's categories into this model instance.
(*3) saveCategories method saves the post's categories specified in
category_idsattribute.
Category model ¶
We want to add a small utility method to Category model.
class Category extends ActiveRecord
{
    ...
    /**
     * Get all the available categories (*4)
     * @return array available categories
     */
    public static function getAvailableCategories()
    {
        $categories = self::find()->order('name')->asArray()->all();
        $items = ArrayHelper::map($categories, 'id', 'name');
        return $items;
    }
}
(*4) getAvailableCategories method is a static utility function to get the list of available categories. In the returned array, the keys are 'id' and the values are 'name' of the categories.
Controller Actions and Views ¶
Since we already have all the necessary logic in models, the controller actions can be as simple as the following examples.
Create action ¶
/**
 * Create Post with its categories
 */
public function actionCreate()
{
    $model = new PostWithCategories();
    
    if ($model->load(Yii::$app->request->post()) {
        if ($model->save()) {
            $model->saveCategories();
            return $this->redirect(['index']);
        }
    }
    return $this->render('create', [
        'model' => $model,
        'categories' => Category::getAvailableCategories(),
    ]);
}
create.php view script ¶
In the view script, we can use a listBox with multiple selection or a checkboxList to select the categories.
<?php $form = ActiveForm::begin([
    'id' => 'post-form',
    'enableAjaxValidation' => false,
]); ?>
<?= $form->field($model, 'title')->textInput(); ?>
<?= $form->field($model, 'body')->textArea(); ?>
...
<?= $form->field($model, 'category_ids')
    ->listBox($categories, ['multiple' => true])
    /* or, you may use a checkbox list instead */
    /* ->checkboxList($categories) */
    ->hint('Select the categories of the post.');
?>
<div class="form-group">
    <?= Html::submitButton('Create', [
        'class' => 'btn btn-primary'
    ]) ?>
</div>
<?php ActiveForm::end(); ?>
Update action ¶
It's almost the same with Create action:
/**
 * Update a post with its categories
 * @param integer $id the post's ID
 */
public function actionUpdate($id)
{
    $model = PostWithCategories::findOne($id);
    $model->loadCategories();
    
    if ($model->load(Yii::$app->request->post()) {
        if ($model->save()) {
            $model->saveCategories();
            return $this->redirect(['index']);
        }
    }
    return $this->render('update', [
        'model' => $model,
        'categories' => Category::getAvailableCategories(),
    ]);
}
update.php view script ¶
It's different from create.php only in the text of the submit button:
...
    <?= Html::submitButton('Update', [
        'class' => 'btn btn-primary'
    ]) ?>
...
Listbox
I use view script, and its me help
Thanks alot for a clear and wonderfull examle
it helps me alot
and thank you for all the yii2 framework!
it is great and very usefull
There is a problem: Getting unknown property for 'category_ids'
Hi there is a problem when running this code
base component throws an exception of unknown property for the array that was defined in the extending model class.
how can i solve this?
re: Getting unknown property for 'category_ids'
Probably you are using 'Post' model, because 'category_ids' is not a property of 'Post' but 'PostWithCategories'.
should be
public $category_ids = [];If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.