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
post
table)- id
- title
- body
- ... etc.
- Category extends ActiveRecord (representing
category
table)- id
- name
- ... etc.
- PostCategory extends ActiveRecord (representing
post_category
junction 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_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'
]) ?>
...
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.