Render checkbox list with initially checked items in Yii2

This guide shows, how to (in Yii2) render checkbox list with correctly selected items (preselected during form render), using both HTML helper classes and active form approach.

This is an extended version of this Stack Overflow question and an answer following it, plus some additional information from me and from other sources.

Before we start

I use the example, where there are many laboratories and many users. Each user can be bind to n laboratories, through user_id in laboratory table. But, each laboratory can have only one user bind to it. Therefore, this is 1-to-n relation and there is no mid-table.

We want to display checkbox list of all laboratories available, in user update form, with correctly checked all laboratories, to which particular user is bind. Therefore, I assume, that we have User and Lab models. User model has following relation declared:

public function getLab()
{
    return $this->hasMany(Lab::className(), ['user_id' => 'id']);
}

And Lab model has following relation:

public function getUser()
{
    return $this->hasOne(User::className(), ['id' => 'user_id']);
}

Inside any model we can get list of all laboratories available, using Lab model for example:

$allLabs = ArrayHelper::map(appmodelsLab::find()->all(), 'id', 'name');

Returned list is an array of ActiveRecord objects.

Note: First examples here are shown inside a controller. Because the very same operations are made for both create and update actions, and to satisfy MVC architectural pattern, you’re strong advised to put this code inside model (see end fo this doc).

The only exception from this rule (keeping this code in controller) is when you actually call it only in one action. For example, you allow modification on relation only during update process, never during creation of new master model.

Checkbox list as a part of an active form

Yii2 does most of the dirty work, so rendering checkbox list inside active form for related models is easy:

<?= $form->field($model, 'lab')->checkboxList($allLabs); ?>

Where lab corresponds to getLab() relation defined in User model (above).

This relation is all, that Yii2 needs to render checkbox list. You only have to take care for updating related models. Fortunately, this is an easy task, thanks to link, unlink and unlinkAll methods, as described in “Saving Relations” section in Yii2 documentation. Here is an example of how this can be handled.

Since you’re now using active form, you can get your hand on checkbox list’s data like this:

$postData = Yii::$app->request->post();
$checkedCheckboxes = $postData['User']['lab'];

Or like this (if you have PHP 5.4.0 or newer):

$checkedCheckboxes = $Yii::$app->request->post()['User']['lab'];

Now, parse this array and do the job:

$model->unlinkAll('lab');

foreach ($postData['User']['lab'] as $labId) {
    $lab = Lab::findOne($labId);

    if ($lab instanceof appmodelsLab) {
        $model->link('lab', $lab);
    }
}

Or, in even shorter way:

$model->unlinkAll('lab');

foreach ($postData['User']['lab'] as $labId) {
    $model->link('lab', Lab::findOne($labId));
}

if you don’t care for extra checkings.

Rendering checkbox list using HTML helpers

Solving this problem using HTML helper classes is a bit more complex.

The yiihelpersBaseHtml::checkboxList method requires us to provide it with three arguments. An id of rendered form element, list of checked items and list of all items. Since we’re not using active form approach, we need provide list of checked items ourselves:

$currentLabs = ArrayHelper::map($this->getLab()->all(), 'id', 'name');

Second list (all items) should be in form of associative array, while first one (checked items) as simple array.

Like this:

$allLabs = Array
(
    [1] => UHV Laboratory
    [2] => TST Laboratory
    [3] => THREE Laboratory
)

$currentLabs = Array
(
    [0] => 1
    [1] => 2
)

First array should contain values of each checkbox as array keys and labels as array values. Second array should be a simple list array, that contains values of these items, that should be initially checked. Since second array must be a simple array, not an associative one, so you have to “flatten” it, getting only values and dropping keys:

$currentLabs = array_keys(ArrayHelper::map($this->getLab()->all(), 'id', 'name'));

Having these, all that is left to do, is to render checkbox list:

yiihelpersBaseHtml::checkboxList('labs', $currentLabs, $allLabs);

As a result, a piece of HTML code, like following, should be generated for you:

<div>
    <label><input type="checkbox" name="labs[]" value="1" checked=""/> UHV Laboratory</label>
    <label><input type="checkbox" name="labs[]" value="2" checked=""/> TST Laboratory</label>
    <label><input type="checkbox" name="labs[]" value="3"/> THREE Laboratory</label>
</div>

Take a look, how values and other elements in generated HTML code corresponds to data passed in each of two arrays.

Reading data out of form

Since, in this example, we used labs as an ID of generated checkbox list, then in the controller, we can read, what user has actually checked in the form, buy analysing:

$postData = Yii::$app->request->post();
$checkedCheckboxes = $postData['labs'];

Or, if you have PHP 5.4.0 or newer, then simple by reading:

$checkedCheckboxes = $Yii::$app->request->post()['labs'];

As a result, you’ll get an array in following form:

Array
(
    [0] => 1
    [1] => 3
)

Where each item’s value corresponds to value of <input value=""/> for each checkbox, that has been checked. In this example, these are laboratory IDs, read directly from relation using $this->getLab()->all() method.

You need to write your own code, that will parse this array and set proper values in related model. Since you’re not using active form, you won’t do this for you. How you’re going to save it to database, depends highly on, how do you store these values in DB.

If, for example, you store all of them in one field, as a list of strings, separated by comma, you can use this approach for a neat solution to handle both reads and writes from/to database.

MVC!

According to MVC architectural pattern, this kind of operations (update of related data) should be performed inside model. Yii comes handy for this with its events subsystem. Here is an example of beforeSave() event, that handles update of related data, discussed above:

public function afterSave($insert, $changedAttributes)
{
    $postData = Yii::$app->request->post();

    if (isset($postData['User']['lab']) && is_array($postData['User']['lab']) && count($postData['User']['lab']) > 0) {
        $this->unlinkAll('lab');

        foreach ($formData as $labId) {
            $lab = Lab::findOne($labId);

            if ($lab instanceof appmodelsLab) {
                $this->link('lab', $lab);
            }
        }
    }

    parent::afterSave($insert, $changedAttributes);
}

For the reasons explained here, this task is performed in afterSave() event, not in beforeSave() event or in save() method.

Since, we’re talking about related models, you can’t forget about unlinking them before deleting master model. Another event comes handy for this:

public function beforeDelete()
{
    if (parent::beforeDelete()) {
        $this->unlinkAll('lab');

        return true;
    } else {
        return false;
    }
}

Put both to your model and you should be done. With just these two events you have implemented full handling of related model. It should be correctly updated on both create and update of master model. And they should be correctly released on master record’s delete.

Leave a Reply