How to create/update a model with its related items using Listbox or CheckboxList

You are viewing revision #12 of this wiki article.
This is the latest version of this article.
You may want to see the changes made in this revision.

« previous (#11)

  1. Scenario
  2. Models
  3. Controller Actions and Views

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.

  1. Post extends ActiveRecord (representing post table)
    • id
    • title
    • body
    • ... etc.
  2. Category extends ActiveRecord (representing category table)
    • id
    • name
    • ... etc.
  3. PostCategory extends ActiveRecord (representing post_category junction table)
    • post_id
    • category_id
  4. 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_ids attribute.

(*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_ids attribute.

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'
    ]) ?>
...
5 0
6 followers
Viewed: 87 991 times
Version: 2.0
Category: Tutorials
Written by: softark
Last updated by: softark
Created on: Jan 27, 2016
Last updated: 6 years ago
Update Article

Revisions

View all history