Scenario
Assume we want to create a page that gathers a user's favorite foods.
There are many users, and many foods too. And we define users' favorite foods using a junction table.
Models
Here are 4 models involved in the page we want to create.
- User extends ActiveRecord (representing
user
table)- id
- name
- ... etc.
- Food extends ActiveRecord (representing
food
table)- id
- name
- ... etc.
- FavoriteFood extends ActiveRecord (representing
user_food
junction table)- user_id
- food_id
- UserFavorites extends Model
- user_id
- food_ids
The first 3 models are ActiveRecords. You can create them easily with the help of Gii.
But the last one is not an ActiveRecord. It doesn't have a table that it represents. We use it for the form of the page. We have to create it on our own.
Note that 'food_ids' attribute is an array of food ids.
Model for the Form
class UserFavorites extends Model { /** * @var integer user ID */ $user_id; /** * @var array IDs of the favorite foods */ $food_ids = []; /** * @return array the validation rules. */ public function rules() { return [ ['user_id', 'required'], ['user_id', 'exist', 'targetClass' => User::className(), 'targetAttribute' => 'id'], // each food_id must exist in food table (*1) ['food_ids', 'each', 'rule' => [ 'exist', 'targetClass' => Food::className(), 'targetAttribute' => 'id' ]], ]; } /** * @return array customized attribute labels */ public function attributeLabels() { return [ 'user_id' => 'User', 'food_ids' => 'Favorite Foods', ]; } /** * load the user's favorite foods (*2) */ public function loadFavorites() { $this->food_ids = []; $favfoods = FavoriteFood::find()->where(['user_id' => $this->user_id])->all(); foreach($favfoods as $ff) { $this->food_ids[] = $ff->food_id; } } /** * save the user's favorite foods (*3) */ public function saveFavorites() { /* clear the favorite foods of the user before saving */ FavoriteFood::deleteAll(['user_id' => $this->user_id]); if (is_array($this->food_ids)) { foreach($this->food_ids as $food_id) { $ff = new FavoriteFood(); $ff->user_id = $this->user_id; $ff->food_id = $food_id; $ff->save(); } } /* Be careful, $this->food_ids can be empty */ } /** * Get all the available foods (*4) * @return array available foods */ public static function getAvailableFoods() { $foods = Food::find()->order('name')->asArray()->all(); $items = ArrayHelper::map($foods, 'id', 'name'); return $items; } }
(*1) In the rules for the validation, we use EachValidator to validate the array of
food_ids
attribute.(*2) loadFavorites method loads the IDs of the user's favorite foods into this model instance.
(*3) saveFavorites method saves the user's favorite foods specified in
food_ids
attribute.(*4) getAvailableFoods method is an static utility function to get the list of available foods. In the returned array, the keys are 'id' and the values are 'name' of the foods.
Controller
Since we have already implemented all the necessary logic in UserFavorites
model, the controller action can be as simple as the following:
/** * Gathers the favorite foods of the specified user * @param integer $user_id the user's ID */ public function actionFavoriteFood($user_id) { $model = new UserFavorites(); $model->user_id = $user_id; if ($model->load(Yii::$app->request->post()) { if ($model->validate()) { $model->saveFavorites(); return $this->redirect(['index']); } } $model->loadFavorites(); $items = UserFavorites::getAvailableFoods(); return $this->render('favorite', [ 'model' => $model, 'items' => $items, ]); }
View
In the view script, we can use a listBox with multiple selection, or a checkboxList to select the favorite foods.
$form = ActiveForm::begin([ 'id' => 'favorite-form', 'enableAjaxValidation' => false, ]); <?= Html::activeHiddenInput($model, 'user_id') <?= $form->field($model, 'food_ids') ->listBox($items, ['multiple' => true]) /* or, you may use a checkbox list instead */ /* ->checkboxList($items) */ ->hint('Select the favorite foods.'); <div class="form-group"> <?= Html::submitButton('Update', [ 'class' => 'btn btn-primary' ]) </div> <?php ActiveForm::end();