How to save multiple related models in Yii [Complete Solution]

Scenario:
You are in a situation where you have two related tables a Parent and a child table. You need to create a user experience in which user presses Save button only once, meaning by, user enters all the data in parent model fields, then enters data in all child model fields, optionally generates more child model fields and then presses the save button.

Lengthy Approach:
To save multiple related models, you need to start a transaction and save child models inside loop. After saving all data of parent and all its child you commit the transaction. This technique is good but not the best, as you have to repeat this procedure every time come across saving multiple related models in your Yii application.

Better to follow code reuse Approach:
There are a lot of Yii extensions available out there that solves common problems. Why not use some available extension and save both time and effort? There is a good extension available that saves multiple related models for you, no need to start transactions and iterate through loops. This makes your code easy to read and also reduce redundancy.

esaverelatedbehavior developed by sluderitz for saving related models, you can download it here
http://www.yiiframework.com/extension/esaverelatedbehavior/

Table Structure:

CREATE TABLE IF NOT EXISTS `fathers` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

CREATE TABLE IF NOT EXISTS `children` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `father_id` int(11) NOT NULL,
  `name` varchar(255) NOT NULL,
  `age` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `father_id` (`father_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

ALTER TABLE `children`
  ADD CONSTRAINT `children_ibfk_1` FOREIGN KEY (`father_id`) REFERENCES `fathers` (`id`) ON DELETE CASCADE;


Father Model:

/**
 * This is the model class for table "fathers".
 *
 * The followings are the available columns in table 'fathers':
 * @property integer $id
 * @property string $name
 *
 * The followings are the available model relations:
 * @property Children[] $childrens
 */
class Father extends CActiveRecord
{

    /**
     * Returns the static model of the specified AR class.
     * @param string $className active record class name.
     * @return Father the static model class
     */
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }

    /**
     * @return string the associated database table name
     */
    public function tableName()
    {
        return 'fathers';
    }

    /**
     * @return array validation rules for model attributes.
     */
    public function rules()
    {
        // NOTE: you should only define rules for those attributes that
        // will receive user inputs.
        return array(
            array('name', 'required'),
            array('name', 'length', 'max' => 255),
            // The following rule is used by search().
            // Please remove those attributes that should not be searched.
            array('id, name', 'safe', 'on' => 'search'),
        );
    }

    /**
     * @return array relational rules.
     */
    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(
            'children' => array(self::HAS_MANY, 'Child', 'father_id'),
        );
    }

    /**
     * @return array customized attribute labels (name=>label)
     */
    public function attributeLabels()
    {
        return array(
            'id' => 'ID',
            'name' => 'Name',
        );
    }

    /**
     * Retrieves a list of models based on the current search/filter conditions.
     * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
     */
    public function search()
    {
        // Warning: Please modify the following code to remove attributes that
        // should not be searched.

        $criteria = new CDbCriteria;

        $criteria->compare('id', $this->id);
        $criteria->compare('name', $this->name, true);

        return new CActiveDataProvider($this, array(
                    'criteria' => $criteria,
                ));
    }

    public function behaviors()
    {
        return array('ESaveRelatedBehavior' => array(
                'class' => 'application.components.ESaveRelatedBehavior')
        );
    }

}

Child Model:


<?php

/**
 * This is the model class for table "children".
 *
 * The followings are the available columns in table 'children':
 * @property integer $id
 * @property integer $father_id
 * @property string $name
 * @property integer $age
 *
 * The followings are the available model relations:
 * @property Fathers $father
 */
class Child extends CActiveRecord
{

    /**
     * Returns the static model of the specified AR class.
     * @param string $className active record class name.
     * @return Child the static model class
     */
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }

    /**
     * @return string the associated database table name
     */
    public function tableName()
    {
        return 'children';
    }

    /**
     * @return array validation rules for model attributes.
     */
    public function rules()
    {
        // NOTE: you should only define rules for those attributes that
        // will receive user inputs.
        return array(
            array('father_id, name, age', 'required'),
            array('father_id, age', 'numerical', 'integerOnly' => true),
            array('name', 'length', 'max' => 255),
            // The following rule is used by search().
            // Please remove those attributes that should not be searched.
            array('id, father_id, name, age', 'safe', 'on' => 'search'),
        );
    }

    /**
     * @return array relational rules.
     */
    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(
            'father' => array(self::BELONGS_TO, 'Father', 'father_id'),
        );
    }

    /**
     * @return array customized attribute labels (name=>label)
     */
    public function attributeLabels()
    {
        return array(
            'id' => 'ID',
            'father_id' => 'Father',
            'name' => 'Name',
            'age' => 'Age',
        );
    }

    /**
     * Retrieves a list of models based on the current search/filter conditions.
     * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
     */
    public function search()
    {
        // Warning: Please modify the following code to remove attributes that
        // should not be searched.

        $criteria = new CDbCriteria;

        $criteria->compare('id', $this->id);
        $criteria->compare('father_id', $this->father_id);
        $criteria->compare('name', $this->name, true);
        $criteria->compare('age', $this->age);

        return new CActiveDataProvider($this, array(
                    'criteria' => $criteria,
                ));
    }

    

}

Father Controller:

<?php

class FatherController extends Controller
{

    /**
     * @var string the default layout for the views. Defaults to '//layouts/column2', meaning
     * using two-column layout. See 'protected/views/layouts/column2.php'.
     */
    public $layout = '//layouts/column2';

    /**
     * @return array action filters
     */
    public function filters()
    {
        return array(
            'accessControl', // perform access control for CRUD operations
            'postOnly + delete', // we only allow deletion via POST request
        );
    }

    /**
     * Specifies the access control rules.
     * This method is used by the 'accessControl' filter.
     * @return array access control rules
     */
    public function accessRules()
    {
        return array(
            array('allow', // allow all users to perform 'index' and 'view' actions
                'actions' => array('index', 'view'),
                'users' => array('*'),
            ),
            array('allow', // allow authenticated user to perform 'create' and 'update' actions
                'actions' => array('create', 'update', 'loadChildByAjax'),
                'users' => array('@'),
            ),
            array('allow', // allow admin user to perform 'admin' and 'delete' actions
                'actions' => array('admin', 'delete'),
                'users' => array('admin'),
            ),
            array('deny', // deny all users
                'users' => array('*'),
            ),
        );
    }

    /**
     * Displays a particular model.
     * @param integer $id the ID of the model to be displayed
     */
    public function actionView($id)
    {
        $this->render('view', array(
            'model' => $this->loadModel($id),
        ));
    }

    /**
     * Creates a new model.
     * If creation is successful, the browser will be redirected to the 'view' page.
     */
    public function actionCreate()
    {
        $model = new Father;

        // Uncomment the following line if AJAX validation is needed
        $this->performAjaxValidation($model);

        if (isset($_POST['Father']))
        {
            $model->attributes = $_POST['Father'];

            if (isset($_POST['Child']))
            {
                $model->children = $_POST['Child'];
            }
            if ($model->saveWithRelated('children'))
                $this->redirect(array('view', 'id' => $model->id));
            else
                $model->addError('children', 'Error occured while saving children.');
        }

        $this->render('create', array(
            'model' => $model,
        ));
    }

    /**
     * Updates a particular model.
     * If update is successful, the browser will be redirected to the 'view' page.
     * @param integer $id the ID of the model to be updated
     */
    public function actionUpdate($id)
    {
        $model = $this->loadModel($id);

        // Uncomment the following line if AJAX validation is needed
        // $this->performAjaxValidation($model);

        if (isset($_POST['Father']))
        {
            $model->attributes = $_POST['Father'];
            if (isset($_POST['Child']))
            {
                $model->children = $_POST['Child'];
            }
            if ($model->saveWithRelated('children'))
                $this->redirect(array('view', 'id' => $model->id));
            else
                $model->addError('children', 'Error occured while saving children.');
        }

        $this->render('update', array(
            'model' => $model,
        ));
    }

    /**
     * Deletes a particular model.
     * If deletion is successful, the browser will be redirected to the 'admin' page.
     * @param integer $id the ID of the model to be deleted
     */
    public function actionDelete($id)
    {
        $this->loadModel($id)->delete();

        // if AJAX request (triggered by deletion via admin grid view), we should not redirect the browser
        if (!isset($_GET['ajax']))
            $this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('admin'));
    }

    /**
     * Lists all models.
     */
    public function actionIndex()
    {
        $dataProvider = new CActiveDataProvider('Father');
        $this->render('index', array(
            'dataProvider' => $dataProvider,
        ));
    }

    /**
     * Manages all models.
     */
    public function actionAdmin()
    {
        $model = new Father('search');
        $model->unsetAttributes();  // clear any default values
        if (isset($_GET['Father']))
            $model->attributes = $_GET['Father'];

        $this->render('admin', array(
            'model' => $model,
        ));
    }

    /**
     * Returns the data model based on the primary key given in the GET variable.
     * If the data model is not found, an HTTP exception will be raised.
     * @param integer the ID of the model to be loaded
     */
    public function loadModel($id)
    {
        $model = Father::model()->findByPk($id);
        if ($model === null)
            throw new CHttpException(404, 'The requested page does not exist.');
        return $model;
    }

    /**
     * Performs the AJAX validation.
     * @param CModel the model to be validated
     */
    protected function performAjaxValidation($model)
    {
        if (isset($_POST['ajax']) && $_POST['ajax'] === 'father-form')
        {
            echo CActiveForm::validate($model);
            Yii::app()->end();
        }
    }

    public function actionLoadChildByAjax($index)
    {
        $model = new Child;
        $this->renderPartial('child/_form', array(
            'model' => $model,
            'index' => $index,
//            'display' => 'block',
        ), false, true);
    }

}

Create Father Form:

<?php
/* @var $this FatherController */
/* @var $model Father */
/* @var $form CActiveForm */
?>

<div class="form">

    <?php
    $form = $this->beginWidget('CActiveForm', array(
        'id' => 'father-form',
        'focus' => array($model, 'name'),
        'enableClientValidation' => true,
        'enableAjaxValidation' => true,
            ));
    ?>

    <p class="note">Fields with <span class="required">*</span> are required.</p>

    <?php echo $form->errorSummary($model); ?>

    <div class="row">
        <?php echo $form->labelEx($model, 'name'); ?>
        <?php echo $form->textField($model, 'name', array('size' => 60, 'maxlength' => 255)); ?>
        <?php echo $form->error($model, 'name'); ?>
    </div>

    <?php
    echo CHtml::link('Add Child', '#', array('id' => 'loadChildByAjax'));
    ?>
    <div id="children">
        <?php
        $index = 0;
        foreach ($model->children as $id => $child):
            $this->renderPartial('child/_form', array(
                'model' => $child,
                'index' => $id,
                'display' => 'block'
            ));
            $index++;
        endforeach;
        ?>
    </div>

    <div style="clear:both;"></div>
    <div class="row buttons">
        <?php echo CHtml::submitButton($model->isNewRecord ? 'Create' : 'Save'); ?>
    </div>

    <?php $this->endWidget(); ?>

</div><!-- form -->

<?php
Yii::app()->clientScript->registerCoreScript('jquery');
Yii::app()->clientScript->registerScript('loadchild', '
var _index = ' . $index . ';
$("#loadChildByAjax").click(function(e){
    e.preventDefault();
    var _url = "' . Yii::app()->controller->createUrl("loadChildByAjax", array("load_for" => $this->action->id)) . '&index="+_index;
    $.ajax({
        url: _url,
        success:function(response){
            $("#children").append(response);
            $("#children .crow").last().animate({
                opacity : 1, 
                left: "+50", 
                height: "toggle"
            });
        }
    });
    _index++;
});
', CClientScript::POS_END);
?>

Child form: views\father\child\_form.php

<div style="margin-bottom: 20px; display: <?php echo!empty($display) ? $display : 'none'; ?>; width:100%; clear:left;" class="crow">

    <div class="row" style="width:200px;float: left;">
        <?php echo CHtml::activeLabelEx($model, '[' . $index . ']name'); ?>
        <?php echo CHtml::activeTextField($model, '[' . $index . ']name', array('size' => 20, 'maxlength' => 255)); ?>
        <?php echo CHtml::error($model, '[' . $index . ']name'); ?>
    </div>

    <div class="row" style="width:200px;float: left;">
        <?php echo CHtml::activeLabelEx($model, '[' . $index . ']age'); ?>
        <?php echo CHtml::activeTextField($model, '[' . $index . ']age'); ?>
        <?php echo CHtml::error($model, '[' . $index . ']age'); ?>
    </div>
    <div class="row" style="width:100px;float: left;">
        <br />
        <?php echo CHtml::link('Delete', '#', array('onclick' => 'deleteChild(this, ' . $index . '); return false;'));
        ?>
    </div>
</div>

<?php
Yii::app()->clientScript->registerScript('deleteChild', "
function deleteChild(elm, index)
{
    element=$(elm).parent().parent();
    /* animate div */
    $(element).animate(
    {
        opacity: 0.25, 
        left: '+=50', 
        height: 'toggle'
    }, 500,
    function() {
        /* remove div */
        $(element).remove();
    });
}", CClientScript::POS_END);

This solution uses esaverelatedbehavior developed by sluderitz for saving related models, you can download it here
http://www.yiiframework.com/extension/esaverelatedbehavior/

About Author

You may also like

17 Response Comments

  • Hayk  August 20, 2013 at 8:03 am

    Does the client side validations work with multiforms ??

    Reply
  • Dibyo  November 29, 2013 at 2:11 am

    thanks tahiryasin…
    this tutorial complete my job…

    Reply
  • patrice  March 6, 2014 at 4:41 pm

    Nice job.

    I found a little problem when updating a form, in the child table.
    I have a father, id = 1
    who has 4 children, id : 5,6,7,8

    When i update the form, children’s ids change:
    same children with no modification but ids are now : 9,10,11,12.

    Each time i update, children’s ids change, the autoincrement works but for nothing.

    Any idea to keep the same ids ?

    Thanks
    patrice@flaho.fr

    Reply
    • tahiryasin  March 7, 2014 at 11:02 am

      ESaveRelatedBehavior component is well documented, its mentioned in the class that If the relation name is used as index with ‘append’ as value (relationName =>’append’) then existing records will not be deleted before inserting.

      Example Usage:

      $model->saveWithRelated(array('children'=>'append'))
      Reply
      • pdebarnot  March 26, 2014 at 10:48 am

        I don’t think this solves patrice’s problem though. He doesn’t want to replace the current relations or append new ones, he wants to update the relations’ data without having to delete and recreate them (this is important for id preservation, I’m having the same issue right).

        Unfortunately, the behavior doesn’t seem to support this feature yet.

        Reply
        • tahiryasin  March 26, 2014 at 4:09 pm

          It will not update the ids when you add ‘children’=>’append’

          100% working for me!

          Reply
    • pdebarnot  March 26, 2014 at 5:19 pm

      I had this same issue as I stated below, and found out why it was happening.
      First make sure you include a hidden input in child/_form.php to store the children ids. I already had this but the ids were still being altered on submit. It was actually because of the model validation: you need to add a ‘safe’ rule for the id field in the child model (in Tahir’s code there is one but it’s only for the scenario ‘search’).

      Basically, when you apply massive assignment to a model in Yii, all fields that don’t have any validation rule will be ignored during the assignment. Which, in our case, leads to MySQL applying the current auto-increment value to the relations we save.

      Reply
  • charlie  July 4, 2014 at 6:56 am

    is there a precise coding for this how about creating a bit like this:

    if($model->save(false)){
    $relatedModel = new [Modelname];
    $relatedModel['user_id'] = $model->id;
    and so forth
    }

    Reply
    • Tahir Yasin  July 4, 2014 at 8:17 am

      This may create database inconsistency, because there is no role back mechanism in this technique. For example your first $model saves without any error but some models inside if block may arise validation errors and don’t save. So the code you shared may partially save some models, and this should be addressed.

      Reply
  • naveed  July 24, 2014 at 8:44 am

    How could we use this extension for only one model.
    Actually I want the same settings for a single model. i.e. I want to add more than one instances of the same model at one time just like in this example.

    Reply
    • Tahir Yasin  July 24, 2014 at 9:36 am

      Naveed,
      You can start a transaction and then save all models in a loop and then commit the transaction.

      Reply
      • naved  August 11, 2014 at 12:27 pm

        Would you please give me a practical example ?
        If possible, then on my Email.

        Reply
  • annegii  August 13, 2014 at 8:49 am

    you save my day!

    THANKS A BUNCH!

    Reply
  • annegii  August 14, 2014 at 4:18 pm

    hello.. everything works!
    But I have problem when updating data… parent’s attribute fields are blank (couldn’t be load).. :(
    here my codes:
    http://pastebin.com/iK4sLT3S

    help me please :(

    Reply
  • Firman Alif  September 5, 2014 at 9:47 am

    hello,
    I try to use but failed, if one table has a foreign key and do not have a primary key, what can be used in this coding?

    Reply

Leave A Comment

Please enter your name. Please enter an valid email address. Please enter a message.