Skip to content


Handling Related Models in Yii Forms

Normally two Models in an MVC architecture are related to each other, such as Employees and Departments (to use the classic example), where each employee is in one department and each department has multiple employees. Although Yii does a great job of auto-generating most of the code you need, including the form used to create and update a Model, the Yii-generated form won’t properly represent the related Model. In this post I’ll walk you through what you need to do to make your forms work properly for related Models.

Looking at the employees example, you would likely have a departmentId value in the Employees table: a foreign key (FK) to store the id value from the Departments table, thereby representing the department that the employee is in. That’s perfect. The form generated by Yii, though, will create a text input for creating and updating the employee’s department:

<div>
 <?php echo CHtml::activeLabelEx($model,'departmentId'); ?>
 <?php echo CHtml::activeTextField($model,'departmentId'); ?>
 <?php echo CHtml::error($model,'departmentId'); ?>
 </div>

That’s not appropriate, of course. The department value for the employee must be a number and you can’t expect the user to know the primary key numbers of the different departments. It’d be most appropriate to turn that text input into a drop-down menu. That’s not hard to do:

<div>
<?php echo $form->labelEx($model,'departmentId'); ?>
<?php echo $form->dropDownList($model, 'departmentId', CHtml::listData(
Departments::model()->findAll(), 'id', 'name'),
array('prompt' => 'Select a Department')
); ?>
<?php echo $form->error($model,'departmentId'); ?>
</div>

And that’s all you need to do to get this to work. The dropDownList() method creates a SELECT menu. It’s associated with the departmentId attribute of this Model. The drop-down menu’s data has to come from CHtml::listData(). That part of the code says to fetch every Department, and to use the department’s id value for the menu value and its name value for the visible label. The final argument indicates a default prompt to give the menu. Since the Employees Model has a departmentId attribute, the framework will be able to handle errors, pre-select the right value on an update, and so forth. If you change the attribute label for departmentId in protected/models/Employees.php, you can make the prompt say “Department” instead. No problem.

A more complicated situation exists when there’s a many-to-many relationship between two Models, such as Posts and Categories, in a blog site. Because of the many-to-many relationship between these two, neither Posts nor Categories would have a foreign key to the other. Instead, a junction table (and Model) would be used. Let’s call that PostsCategories. The table itself would only need two columns: postId and categoryId.For each category that a post is associated with, there would be a record in this table.

On the form for adding (or updating) a blog post, you’d need to be able to select multiple Categories:

<div>
<?php echo $form->labelEx($model,'categories'); ?>
<?php echo $form->dropDownList($model, 'categories', CHtml::listData(
Categories::model()->findAll(), 'id', 'category'), array('multiple'=>'multiple', 'size'=>5)
); ?>
<?php echo $form->error($model,'categories'); ?>
</div>

That dropDownList() method will create a drop-down of size 5 (five items will be shown), populated using the list of Categories, and the user will be able to select multiple options. If you were to run this code, though, you’d get an error as Posts doesn’t have a categories attribute. But that’s easy to change…

In the Model definition, you identify the relationship to other Models within the relations() method. The relationship between Posts and PostsCategories could be represented as:

// protected/models/Posts
public function relations() {
    return array(
        'categories' => array(self::HAS_MANY, 'Categories', 'postId')
    );
}

Now Posts has a categories attribute, meaning that the above form code will work. In fact, you can also add categories to the attributeLabels() array to give this relationship a new label that would be used in the form. You can even add a rule to make sure this part of the form validates. This is an important enough concept that I want to repeat it: when you create a relation, that relation becomes an attribute of the Model.

In order for the creation of a new Post to work, you’ll need to update the Controller to handle the PostsCategories. Within the actionCreate() method (of PostsController), after the Post has been saved, loop through each category and add that record to PostsCategories. The beginning of actionCreate() would look like:

public function actionCreate() {
    $model=new Posts;
    if(isset($_POST['Posts'])) {
        $model->attributes=$_POST['Posts'];
        if($model->save()) {
            foreach ($_POST['Posts']['categories'] as $categoryId) {
                $postCategory = new PostsCategories;
                $postCategory->postId = $model->id;
                $postCategory->categoryId = $categoryId;
                if (!$postCategory->save()) print_r($postCategory->errors);
            }

Now, when a new Post is created, one new record is created in PostsCategories for each selected category.

We’re almost done getting this working, except we now need to think about the update process. There are two problems with what we’ve got so far: getting the form to select existing PostsCategories and handling updates of PostsCategories. The form has already been created and populated, but to get it to indicate existing selections, we need to pass along the PostsCategories values. You might think that you can just fetch all the PostsCategories records where postId equals the Model’s id (in other words, perform a with(‘PostsCategories’)-> retrieval), but you can’t. For the drop-down menu in the form to preselect the right values, the form needs to access an array of values, not an array of objects. To achieve that, add some code to the loadModel() method of the PostsController. This method is called for the update, delete, and view actions and just returns a single Model. The Model is loaded using:

$this->_model=Posts::model()->findbyPk($_GET['id']);

After that, within the loadModel() method, you can add this code to fetch the associated PostsCategories:

$criteria=new CDbCriteria;
$criteria->condition='postId=:postId';
$criteria->select = 'categoryId';
$criteria->params=array(':postId'=>$_GET['id']);
$postCategories = PostsCategories::model()->findAll($criteria);

That code selects all of the categoryId values from PostsCategories where the postId value equals this post’s id. Next, we need to turn those results into an array:

$categories = array();
foreach ($postCategories as $category) {
    $categories[] = $category->categoryId;
}

Now, $categories is an array of numeric values, one for each category associated with the post. Just add this to the loaded Model:

$this->_model->categories = $categories;

And now the form will automatically select the existing categories for this post.

The final step is to update the PostsCategories when the form is submitted (and the post is updated). This could be tricky, because the user could add or remove categories, or make no changes to the categories at all. The easiest way to handle all possibilities is to clear out the existing values (for this post) in the PostsCategories table, and then add them in anew. To do that, in actionUpdate() of the PostsController, you would have:

if($model->save()) {
    $criteria=new CDbCriteria;
    $criteria->condition='postId=:postId';
    $criteria->params=array(':postId'=>$model->id);
    PostsCategories::model()->deleteAll($criteria);

Then you use the same foreach loop as in actionCreate() to repopulate the table.

And that’s it. It takes a little thought getting all this going but these changes seem to work fine for me and I haven’t come up with a simpler solution yet. I hope this helps you with your next Yii-based project. As always, thanks for reading and let me know if you have any questions or comments.

Posted in MySQL, PHP, Web Development.

Tagged with , , .


69 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. Stoimen Stoimenov says

    Thank you very much for this information. I was just wondering how to accomplish that in my project.

    • Larry says

      You’re quite welcome. Glad I could help!

  2. Stoimen Stoimenov says

    I did everything as you have described and it worked but I have a little problem.

    The problem is that I am building a multi language website and in this script
    dropDownList($model, ‘departmentId’, CHtml::listData( Departments::model()->findAll(), ‘id’, ‘name’),
    array(‘prompt’ => ‘Select a Department’)
    ); ?>
    The departments are displayed in english (because they are saved like that in the db) but I need to call the names in Yii::t(‘departments’, ‘name’) so that they can be shown in any language. But I can’t achieve that… It looks like the ‘name’ is hard coded in the CHtml::listData…

    I wonder if you could help me.

    • Larry says

      Sorry, I haven’t done anything with multilingual sites in Yii. I wouldn’t have a clue where to begin!

  3. Paul says

    Hi Larry,

    how do I update a model’s data?

  4. Sune says

    Thank you very much for your tutorial! All of your tutorials are very clear and well-made!

    I am trying to implement a project form where I would like to have a sub-form where one can change the users related to the project. The user and the project table are in a many-many relationship connected by a user_project-table. But in this junction table – a part from the two foreign keys, there is a field for project_role (the users role in the specific project):

    CREATE TABLE `users_projects` (
    `project_id` int(10) NOT NULL,
    `user_id` int(10) NOT NULL,
    `project_role` enum(‘Participant’,'Coordinator’,'Client’,'Other’) NOT NULL,
    PRIMARY KEY (`project_id`,`user_id`),
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

    I would like to make a sub-form with tabular-data for user and project_role, where one can add, edit or remove users, connected to the project as well as assign project roles.

    I have made such things before in php/mysql but in a very unstructured way (not object-oriented and not using the MVC-design) This is relatively new to me as well as Yii. Do you have any hints on how to begin?

    I think that junction tables with more fields are a common in many db data structures and would like to make a tutorial later on this, but by now I am afraid that I am only able to find very messy solutions;-)

    Thanks again! Sune

    • Larry says

      Thanks for the nice words and my apologies for the delayed reply. As for your specific issue, I don’t think it’ll be that hard to implement. I guess I’d create a series of checkboxes, one for each possible user. Then you’d need to create a select menu, one for each user, in which the admin could identify the user’s role.

  5. Pastia says

    You could take a look at http://www.yiiframework.com/extension/save-relations-ar-behavior

    It is a helpful extension that I am using!

  6. Oscar says

    Larry, one thing that is very common and which I’d love for you to give a Yii example of (like the above many to many drop down list) is a dependent/cascading/chained set of dropdown lists. Although I have seen two yii extensions on this in the yii extensions repository, I have not been able to get any to work as they I wasn’t sure where to put the code (controller, view, model).

    • Larry says

      Hello Oscar. Thanks for the suggestion. I’ve been doing more Ajax stuff with Yii lately and I think you’re right that this is a topic that merits attention. I’ll see what I can do. Thanks!

  7. Zsolt says

    Thanks for sharing this code!
    It saved me a lot of time!

    • Larry says

      You’re welcome. Thanks for the feedback!

  8. Joe Storm says

    THANK YOU LARRY!!! I thought I am going to spend another day solving this and you help me did it in 30 mins!

    • Larry says

      You’re quite welcome. Thanks for the nice words.

  9. Patrick says

    Hey Larry!

    Awesome tutorial! You really helped me a lot.

    But there is one little problem I can’t get a grip on.

    You say:
    “Just add this to the loaded Model:

    $this->_model->categories = $categories;”

    Where exactly would that be? What do you mean by “loaded Model”?

    I constantly keep getting this Error Message:
    “Property “xController._model” is not defined. ”
    This is related to the codeline in the loadModel() function, where (in your case) you typed
    “$this->_model->categories = $categories;”

    I think knowing where to put that little piece of code exactly “into the loaded model” would solve the problem.

    It would be great if you could help me out here.

    Thank you!
    Patrick

    • Larry says

      Thanks for the nice words. In answer to your question, as I say just above that, I’m taking about the loadModel() method of the PostsController. The “loaded Model” is represented by the variable $this->_model, where $this refers to the current object and _model is a private variable (i.e., attribute) of that object.

      • Chris says

        Thanks a lot! That just solved he problem!

      • Patrick says

        Thank you very much, Larry!

  10. Dan says

    Larry, how come you don’t use the MANY_MANY option for the many to many relationships?

    • Larry says

      Because there’s not a many-to-many relationship between employees and departments. Each department can have many employees but each employee is in only one department. Hence, it’s a one-to-many relationship.

  11. raul says

    Hi Larry. How can I print the categories of a post in index.php?r=posts/ (i mean, on _view.php)????
    Thanks for the tutorials!!

    • Larry says

      Thanks for the nice words and you’re welcome. To print the categories associated with a post, you’d want to select the categories with the posts (using the relation) in the Controller. Then, in the View, you can loop through the categories associated with the specific Model instance. There may be an example of this in the Yii Blog demo.

  12. Kevin says

    Hi Larry
    Thanks for the tutorial ! but how do I retain the checked values when the form is reloaded upon hitting validation error? All my other input fields values are retained but not these “categories” checkboxes ..

    Please help!

    • Larry says

      If setup properly, it should happen automatically. I suspect your code is missing something. If you need help with this, please turn to my forums or the Yii forms.

  13. Kevin says

    I follow your example exactly except that I use $form->checkBoxList instead of dropDownList. Can you please help me at Yii Forum? Thanks!
    http://www.yiiframework.com/forum/index.php?/topic/19300-how-to-retain-checkbox-value-when-the-screen-reload/page__gopid__94723#entry94723

  14. Bonny says

    Hi, This post seem near to what I need but still not getting where I need. I have three models, AutoListings, AutoMakes and AutoModels. Now in the AutoListings which has the foreign keys for AutoMakes(make_id) and AutoModels(model_id). Now am in the AutoListing view file and I want to get the make and the model names for a specific AutoListing. How can I do this if I do echo $model->make_id and echo $model->model_id I get the IDs value but I want to get the really names.
    In my AutoListings we have this relation
    public function relations()
    {
    // NOTE: you may need to adjust the relation name and the related
    // class name for the relations automatically generated below.
    return array(
    ‘model’ => array(self::BELONGS_TO, ‘AutoModels’, ‘model_id’),
    ‘make’ => array(self::BELONGS_TO, ‘AutoMakes’, ‘makes_id’),
    );
    }

    • Bonny says

      I finally got it. Thanks

    • Larry says

      You’d do $model->make_id->name (or whatever attribute is in the AutoMakes model). By the way, shouldn’t listings just relate to AutoMakes and AutoMakes to AutoModels, because the makes are tied to the models?

  15. Mimi says

    Thank you very much for this post !

  16. Bonnie says

    Hi, I see on the actionCreate you have $postCategory = new PostsCategories which mean you have a model for your third table PostsCategories. What is the relationship inside it and what is the relationship in your categories model.

    • Larry says

      I’m not exactly sure what you’re asking here.

  17. Aaron Levin says

    Hi Larry,

    What’s the philosophy behind handling the Many-to-Many DB create in the actionCreate() instead of, say, within a afterSave() ? Or even possibly creating a filter?

    It makes sense to me for it to be in the actionCreate() part of the controller, I just want to know why we wouldn’t want to place the code in those others areas.

    Thank you! I found this post very informative. Thanks again.

    Best,

    Aaron Levin

    • Larry says

      Hello Aaron. Thanks for the nice words and for the question. It’s a good one. Here’s the problem with putting this code in an afterSave() method: you’d be having one Model directly interact with another Model, which I consider to be a no-no. The implication would be, for example, that if you later change Model X, then you’d also have to change the afterSave() method in Model Y, which doesn’t make sense from a design perspective. Plus I feel like that’s a bit too much activity for a Model method, and you’d have to handle errors within the Model somehow. It’s a reasonable question, but that’s my thinking on it.

  18. Lee says

    Hi Larry,
    Realy happ to find your site, it’s so useful .
    But I have a little trouble:
    if PostCategories has one more field named ‘type’( or anything like that), and we want to add field into the view so we can add the value of field ‘type’ into database, can you help me?
    thanks you very much

    • Larry says

      Thanks for the nice words. Much appreciated. I’m not following your question, though. If you need help, please use my support forums or the Yii support forums.

  19. dominic says

    Thanks, just what I was looking for!

  20. Andras says

    Larry, thanks for the explanation. I used these ideas in my code.
    However I believe the PostCategory part could be improved.
    You don’t really use the ‘categories’ from the relations(). The code would work equally well if you just defined
    “public $categories”
    in the model instead of defining it in the relations.

    However it is even nicer to use setter and getter functions in the model, like this:
    public function getCategories(){
    $criteria=new CDbCriteria;
    $criteria->condition=’postId=:postId’;
    $criteria->select = ‘categoryId’;
    $criteria->params=array(‘:postId’=>$this->id);
    $postCategories = PostsCategories::model()->findAll($criteria);
    $categories = array();
    foreach ($postCategories as $category) {
    $categories[] = $category->categoryId;
    }
    return $categories;
    }

    and

    public function setCategories($categories) {
    $criteria=new CDbCriteria;
    $criteria->condition=’postId=:postId’;
    $criteria->params=array(‘:postId’=>$this->id);
    PostsCategories::model()->deleteAll($criteria);
    foreach ($categories as $categoryId) {
    $postCategory = new PostsCategories;
    $postCategory->postId = $this->id;
    $postCategory->categoryId = $categoryId;
    if (!$postCategory->save()) print_r($postCategory->errors);
    }
    }

    In this case you don’t need to do anything special in the loadModel part of the controller, since $categories will be there automatically. When you save the model, you have to call
    $model->categories = $_POST['Posts']['categories'];
    instead of the code mentioned in the article.

    Although I used this solution in a similar environment, (so the principle is correct), but the above code was not tested, so few modifications may be needed.

    There is an alternative solution to the getter part:
    if you define the relation
    ‘my_category’ => array(self::MANY_MANY,’Categories’,'PostCategories(postId,categoryId)’),

    then you can simply have:
    public function getCategories(){
    $categories = array();
    foreach ($this->my_category as $category)
    $categories[] = $category->categoryId;
    }

    • Larry says

      Thanks for your input. Your solution may work, but I would not be inclined to do it that way. First, you’re burying quite a bit of logic, referencing other Models, within a Model, which I would avoid. Second, you’re essentially replicating what Yii will do for you through relations and ActiveRecord. But to each their own, of course.

  21. orb says

    thank you very much . it’s make my mood is good.

  22. BornToDrink says

    Hey, very good post, thank you very much!

  23. Mayank Garg says

    Thanks for the nice information .. I was indeed working on a similar problem. However, in my case I am developing a registration form for a student, where in along with the details of the student (model named PersonalInfo) I accept education details (model named EducationInfo). A student may have many qualifications, hence it is a one-many relationship with educational details being stored in another table with the student id. My doubt is in case of educational details I am accepting 3 fields : degree, passing year and branch, so how can I now handle these two models with a single form .

    • Larry says

      It’d be the same, but instead of representing Model B once in a form that’s otherwise focused on Model A, you’d have three fields for Model B.

  24. Kenzo says

    Hi Larry,
    Just wanna thank you for sharing this, well written and clear, saved me tons of time.
    Appreciate.

  25. Tony says

    HI, thank you for your information. But I have a question: what if the table recording the MANY-MANY relationship has another attributes except postID & categoryID, i.g the popularity of a post in a specific category denominated pop. It appears hard for me to let my client fill all necessary information, including pop, in just one form? Any suggestion?

  26. Antonio says

    Hy Larry, thank you very much for this very useful tutorial..but i have a problem..i got an error caused by the fact that the tabel has a composite primary key…have you got any idea about this?

    • Larry says

      Yeah, composite keys are an ongoing issue with Yii. As far as I know, you’ll have to identify the relations yourself.

      • Antonio says

        OK thank you very much, for all

  27. Ahmad says

    Hi Larry,
    Thanks for the article. It’s pretty clear. I just have a problem displaying the option labels in the drop list derived from other related models. Here is my code:

    echo $form->dropDownList($model,’businessId’,
    CHtml::listData(Business::model()->findAll(), ‘id’, ‘name’),
    array(‘empty’=>’select business’));

    This works. But I need the labels to show “Business Name, City Name, Country Name” based on the relations in my Business model. As shown in the code below, something like ‘name, city.name, country.name’ does not work. I could not find an answer so far.

    echo $form->dropDownList($model,’businessId’,
    CHtml::listData(Business::model()->with(‘city’,'city.country’)->findAll(), ‘id’, ‘name, city.name, city.country.name’),
    array(‘empty’=>’select business’));

    Thanks.

    • Larry says

      The solution is to change the label values in the related Model definition.

  28. pannet1 says

    Larry,
    I have modeled my many-many based on the above code. Following the above and applying a new rule.
    - if one Category is related to one Post, it cannot be again related to another Post again.
    Once a category is added to the tbl_PostCategory, I update that tbl_Category row as used_status (with a 1), so it does not appear in the dropdown in the next create.
    The problem is with the update, because the selected values from the join table, now disappears.

    dropDownList($model, 'categories', CHtml::listData(
    Categories::model()->findAll(array('condition'=>'used_status'=>1)), 'id', 'category'), array('multiple'=>'multiple', 'size'=>5)
    ); ?>

    Can you please advise how to ‘merge’ the selected categories from the join table and all categories with used_status=1, in the dropDownList() above.

  29. Hemu says

    Thanks Larry, this code really helped me a lot.

  30. Piro says

    Hello, Larry.
    Thanks for this good the article. I’m new with Yii and i need a little help.. i want to print the categories of a post in the view .. can u help me to manage it.
    Thanks

    • Larry says

      Thanks for the nice words on the article, Piro. If you need assistance, please use my support forums or the Yii support forums.

  31. MIchael Kambenga says

    Hi,Larry
    A lot of thanks for this nice tutorial of your…
    Here comes my problem,
    When it comes to update the Post,suppose i have another table that relates to the PostCategories table,But on updating the Post,all the contents of PostCategories are to be deleted first before saving the new ones!!
    This makes the foreign key constraints failure between the PostCategories and that new table…

  32. AyukizZ says

    7 hours searching on the web, and you got the answer !
    Thank you !

  33. aicrag says

    Hi! Larry, thanks for such a great article. Hey, you said: “You can even add a rule to make sure this part of the form validates.”. How can I achieve that validation rule?

    • Larry says

      See either the Yii manual or my post on rules.

  34. Gerhard says

    Hi Larry, thanx again for your great work. Just a question:

    You state:
    The relationship between Posts and PostsCategories could be represented as:
    ‘categories’ => array(self::HAS_MANY, ‘Categories’, ‘postId’)

    Shouldn’t it be:
    ‘categories’ => array(self::HAS_MANY, ‘PostsCategories’, ‘postId’)

    Regards

Continuing the Discussion

  1. Creating Forms with the Yii Framework – Larry Ullman linked to this post on January 20, 2011

    [...] Handling Related Models in Yii Forms [...]

  2. Easy steps to saving relational data through a form in Yii Framework | stefandoorn.nl linked to this post on August 19, 2011

    [...] the relations to the database. A good guide for beginners to the Yii Framework. Find out more here: http://www.larryullman.com/2010/08/10/handling-related-models-in-yii-forms/ Share and [...]

  3. Yii framework - заметки на полях | Заметки Лёвика linked to this post on March 29, 2012

    [...] http://www.larryullman.com/2010/08/10/handling-related-models-in-yii-forms/ – Yii MAnY MANY save Реклама для "поддержания штанов": [...]

If you need quick assistance with a question or problem related to one of my books, please use the support forums instead.

Some HTML is OK

or, reply to this post via trackback.