Form and Related Models - Request for help

Hi,

I’m hoping a member of the Yii forums may be able to shed some light on an issue I’m facing.

OBJECTIVE: Build a user ‘Signup’ form, which adds a new company and user to the application database.

At the moment I am trying this approach:

In controller action:

  • Take form input.

  • Create new Company model.

  • Assign Company model values using $company->attributes = $_POST[‘Company’];

  • Save Company.

  • Create new User model.

  • Add the new company ID to the input array: $_POST[‘User’][‘company_id’] = $company->id;

  • Assign User model values using $user-> attributes = $_POST[‘User’];

  • Save the User.

At the moment this is not working because when I run $user->save(), the model is not trying to save a value for ‘company_id’ into the database at all, dispute this being a required column.

I’ll post my code below because I’m not sure I’ve explained that very well, if anyone can see any obvious problems or knows of a tutorial which covers this please let me know.

Sorry about the massive first post!

  • Controller Action



	public function actionIndex()

	{		

		$user = new User;	

		$company = new Company;


		if($_POST){

		

		    $company->attributes = $_POST['Company'];

		    $user->attributes = $_POST['User'];

		    

		    $_POST['User']['activation_code'] = 'test';

			$_POST['User']['role_id'] = 1;

			$_POST['User']['company_id'] = 1; // Temporary company ID to pass validation	

			

			if($company->validate() && $user->validate()){

			

				if($company->save()){			

				

				$_POST['User']['company_id'] = $company->id; // Update the false company ID with a real one

				

				if($user->save())

					$this->redirect(array('confirmation', 'id'=>$user->id));			

				}

			}else{

				var_dump($company->getErrors());

				var_dump($user->getErrors());	

			}		

		}	


		$this->render('index',array(

			'user'=>$user,

			'company'=>$company,

		));

	}



  • Form



<div class="form">


<?php $form=$this->beginWidget('CActiveForm', array(

	'id'=>'signup-form',

	'enableAjaxValidation'=>false,

)); ?>


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


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

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

	

	<div class="row">

		<?php echo $form->labelEx($user,'title'); ?>

		<?php echo $form->textField($user,'title',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($user,'title'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($user,'email'); ?>

		<?php echo $form->textField($user,'email',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($user,'email'); ?>

	</div>

	

	<div class="row">

		<?php echo $form->labelEx($company,'title'); ?>

		<?php echo $form->textField($company,'title',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($company,'title'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($user,'first_name'); ?>

		<?php echo $form->textField($user,'first_name',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($user,'first_name'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($user,'last_name'); ?>

		<?php echo $form->textField($user,'last_name',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($user,'last_name'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($user,'password'); ?>

		<?php echo $form->passwordField($user,'password',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($user,'password'); ?>

	</div>

	

	<div class="row">

		<?php echo $form->labelEx($user,'password_repeat'); ?>

		<?php echo $form->passwordField($user,'password_repeat',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($user,'password_repeat'); ?>

	</div>


	<div class="row buttons">

		<?php echo CHtml::submitButton('Signup!'); ?>

	</div>


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


</div>



  • User Model



<?php


/**

 * This is the model class for table "users".

 *

 * The followings are the available columns in table 'users':

 * @property integer $id

 * @property string $title

 * @property string $email

 * @property string $first_name

 * @property string $last_name

 * @property string $password

 * @property string $salt

 * @property string $activtion_code

 * @property integer $status

 * @property integer $company_id

 * @property integer $role_id

 *

 * The followings are the available model relations:

 * @property Company $company

 * @property Roles $role

 */

class User extends CActiveRecord

{

	public $password_repeat;


	/**

	 * @return string the associated database table name

	 */

	public function tableName()

	{

		return 'users';

	}


	/**

	 * @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('title, email, first_name, last_name, password, password_repeat', 'required'),

			array('status, company_id, role_id', 'numerical', 'integerOnly'=>true),

			array('title, email, first_name, last_name, password', 'length', 'max'=>45),

			array('title, email', 'unique'),


			// The following rule is used by search().

			// Please remove those attributes that should not be searched.

			array('id, title, email, first_name, last_name, status, company, activation_code', 'safe', 'on'=>'search'),

			array('password', 'compare'),

			array('password_repeat', 'safe'),

		);

	}


	/**

	 * @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(

			'company' => array(self::BELONGS_TO, 'Company', 'company_id'),

			'role' => array(self::BELONGS_TO, 'Roles', 'role_id'),

		);

	}


}

  • Company Model



<?php


/**

 * This is the model class for table "Company".

 *

 * The followings are the available columns in table 'Company':

 * @property integer $id

 * @property string $title

 * @property integer $owner_id

 *

 * The followings are the available model relations:

 * @property Users[] $users

 */

class Company extends CActiveRecord

{

	/**

	 * @return string the associated database table name

	 */

	public function tableName()

	{

		return 'Company';

	}


	/**

	 * @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('title', 'required'),

			array('title', 'length', 'max'=>45),

			array('title', 'unique'),

			// The following rule is used by search().

			// Please remove those attributes that should not be searched.

			array('id, title', '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(

			'users' => array(self::HAS_MANY, 'Users', 'company_id'),

		);

	}


}



I can’t see any company_id field in your form and it’s neither in the rules for your user model, thus your $_POST[‘User’][‘company_id’] will be null,

also try turning


$_POST['User']['company_id'] = $company->id;

around into


$company->id = $_POST['User']['company_id'];

Might have missed some things but those are the things I noticed at first, the blog tutorial provided on this site also covers what you’re trying to do.

Why save it to $_POST data when you can assign it directly to the model?

Try:




public function actionIndex() {               

        $user = new User;       

        $company = new Company;

        if(isset($_POST)){

                $company->attributes = $_POST['Company'];

                $user->attributes = $_POST['User'];

                if($company->save()){

                        $user->activation_code = 'test';

                        $user->role_id = 1;

                        $user->company_id = $company->id; 

                        if($user->save())

                            $this->redirect(array('confirmation', 'id'=>$user->id));                        

                        }

                }           

        }       

        $this->render('index', array('user' => $user, 'company' => $company));

}



Thanks for the comments, very helpful.

I am now sure that the values are being passed into the models correctly, it just seems that there is an issue with linking the form field ‘company’ to the ‘company_id’ attribute in Users.

I don’t know what to put in the form to create the company field. I want the user to enter a company name, not a company ID, and I want the controller to make a new company with that name, and pass the new id to user model, but if I use this code in form:


	

<div class="row">

		<?php echo $form->labelEx($model,'company_id'); ?>

		<?php echo $form->textField($model,'company_id'); ?>

		<?php echo $form->error($model,'company_id'); ?>

	

</div>



I get this error: Company must be an integer.

Or if I use this code:


        

<div class="row">

                <?php echo $form->labelEx($company,'title'); ?>

                <?php echo $form->textField($company,'title',array('size'=>45,'maxlength'=>45)); ?>

                <?php echo $form->error($company,'title'); ?>

</div>



Then the user model throws this error: Company cannot be blank.

Here is my updated code:

Controller:




public function actionIndex()

    {               

		$user = new User;       

		$company = new Company;


    	if(isset($_POST['User'], $_POST['Company']))

    	{

        	// populate input data to $a and $b

        	$user->attributes=$_POST['User'];

        	$company->attributes=$_POST['Company'];

 

 			$user->salt = 'test'; // Fake required attributes to pass validation

 			$user->activation_code = 'test'; // Fake required attributes to pass validation

 	

        	// validate BOTH $a and $b

        	$valid=$user->validate();

        	$valid=$company->validate() && $valid;

 

			if($valid)  

			{  

   				if($company->save(false))  

    			{  

        			$user->id = $company->id;  

        			$user->save(false);  

        			$this->redirect($this->redirect(array('confirmation', 'id'=>$user->id)));  

    			}  

			}

    	}

 

    	$this->render('index',array(

						'user'=>$user,

						'company'=>$company,

		));

    }




User Model:





class User extends CActiveRecord

{


	public $password_repeat;

	

	/**

	 * Returns the static model of the specified AR class.

	 * @return User 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 'Users';

	}


	/**

	 * @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('title, email, first_name, last_name, password, salt, company_id', 'required'),

			array('status, company_id, role_id', 'numerical', 'integerOnly'=>true),

			array('title, email, first_name, last_name, password, salt, activation_code', 'length', 'max'=>45),

			// The following rule is used by search().

			// Please remove those attributes that should not be searched.

			array('id, title, email, first_name, last_name, password, salt, activation_code, status, company_id, role_id', '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(

			'company' => array(self::BELONGS_TO, 'Company', 'company_id'),

			'role' => array(self::BELONGS_TO, 'Roles', 'role_id'),

		);

	}


	/**

	 * @return array customized attribute labels (name=>label)

	 */

	public function attributeLabels()

	{

		return array(

			'id' => 'ID',

			'title' => 'Title',

			'email' => 'Email',

			'first_name' => 'First Name',

			'last_name' => 'Last Name',

			'password' => 'Password',

			'salt' => 'Salt',

			'activation_code' => 'Activtion Code',

			'status' => 'Status',

			'company_id' => 'Company',

			'role_id' => 'Role',

		);

	}




Company Model:







class Company extends CActiveRecord

{

	/**

	 * Returns the static model of the specified AR class.

	 * @return Company 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 'Companies';

	}


	/**

	 * @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('title', 'required'),

			array('title', 'length', 'max'=>45),

			// The following rule is used by search().

			// Please remove those attributes that should not be searched.

			array('id, title', '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(

		);

	}


	/**

	 * @return array customized attribute labels (name=>label)

	 */

	public function attributeLabels()

	{

		return array(

			'id' => 'ID',

			'title' => 'Company',

		);

	}

}



Form:




<div class="form">


<?php $form=$this->beginWidget('CActiveForm', array(

	'id'=>'user-form',

	'enableAjaxValidation'=>false,

)); ?>


	<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,'title'); ?>

		<?php echo $form->textField($model,'title',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($model,'title'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'email'); ?>

		<?php echo $form->textField($model,'email',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($model,'email'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'first_name'); ?>

		<?php echo $form->textField($model,'first_name',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($model,'first_name'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'last_name'); ?>

		<?php echo $form->textField($model,'last_name',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($model,'last_name'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'password'); ?>

		<?php echo $form->passwordField($model,'password',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($model,'password'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'salt'); ?>

		<?php echo $form->textField($model,'salt',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($model,'salt'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'activation_code'); ?>

		<?php echo $form->textField($model,'activation_code',array('size'=>45,'maxlength'=>45)); ?>

		<?php echo $form->error($model,'activation_code'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'status'); ?>

		<?php echo $form->textField($model,'status'); ?>

		<?php echo $form->error($model,'status'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'company_id'); ?>

		<?php echo $form->textField($model,'company_id'); ?>

		<?php echo $form->error($model,'company_id'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'role_id'); ?>

		<?php echo $form->textField($model,'role_id'); ?>

		<?php echo $form->error($model,'role_id'); ?>

	</div>


	<div class="row buttons">

		<?php echo CHtml::submitButton($model->isNewRecord ? 'Create' : 'Save'); ?>

	</div>


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


</div>




Thanks again!

By the way I have followed this tutorial but it’s missing some elements:

http://www.yiiframework.com/wiki/19/how-to-use-a-single-form-to-collect-data-for-two-or-more-models

Ok, I’ve solved it.

for reference here is the controller action.





	public function actionIndex()

    {               

		$user = new User;       

		$company = new Company;


    	if(isset($_POST['User'], $_POST['Company']))

    	{

        	// populate input data to $a and $b

        	$user->attributes=$_POST['User'];

        	$company->attributes=$_POST['Company'];

 

 			$user->salt = 'test'; // Fake required attributes to pass validation

 			$user->activation_code = 'test'; // Fake required attributes to pass validation

 			$user->company_id = 0;

 	

        	// validate BOTH $a and $b

        	$valid=$user->validate();

        	$valid=$company->validate() && $valid;

 

			if($valid)  

			{  

   				if($company->save(false))  

    			{  

        			$user->company_id = $company->id;  

        			$user->save(false);  

        			$this->redirect($this->redirect(array('confirmation', 'id'=>$user->company_id)));  

    			}  

			}

    	}

 

    	$this->render('index',array(

						'user'=>$user,

						'company'=>$company,

		));

    }




This has been a super helpful post. Thank you for taking the time to lay it all out! I’ve used other frameworks (CakePHP and Zend), but I’m new to Yii. (It definitely suits me better than the others, so far, btw.) I’ve got a couple of questions about your solution (which I’ve adapted to my app, so thank you :) )

It looks like what you’ve got works for a create, but not necessarily update. That is, you’re instantiating new versions of your models and saving them if $_POST includes their attributes, but it’s not taking into account an existing User. I’m working on the update version of a similar situation, and I think it’s working, but I want to make sure I’ve got the gist of it. Two main questions:

  1. In the form, I don’t want to include the id (e.g., your company_id field). I want to have a text form that only displays attributes relevant to the user. In the case of your example, it might be the company_name field, rather than company_id. Do I still need to include the primary key for that model’s table, say in a hidden field? I’m pretty sure actionUpdate be able to get the needed info by calling something like:

$user->company->save()

Does anyone see any potential pitfalls with that? I’d appreciate feedback in case I’m missing something. I don’t think I need the id field for company if I do this, as I’ve got access to the related Company object through the User object.

  1. On a related note, I’m experiencing frustration when trying to access a relation that doesn’t exist, yet. For example, if your Company value was optional for a User, an administrative user might create a new User and save without adding a Company the first time. But when you go back, trying to get the Company via eager loading like this:

$user = User::model()->with('company')->findByPk($user_id)

is problematic if the company doesn’t yet exist, because the request for the db record returns null. So, you get NULL, not even an empty Company object.

I’ve seen a number of proposed solutions for this, but I find it frustrating that I would even need to consider a situation like “get me the Company, but if it’s NULL, make me a new Company.” I just want to say, “Get me the User’s Company, NULL or otherwise.” At the moment, I’m explicitly checking for the existence of relations in the loadModel() method of the controller I’m working on. It looks like this:


	

public function loadModel($id)

	{

		$model=Entity::model()->findByPk($id);

		if($model===null)

			throw new CHttpException(404,'The requested page does not exist.');

		// Load the rest of the pieces. Was going to use 'with' method, but it seems not to work

		// if the relations return a NULL record (e.g., phone is not created yet).

		// @todo Use ActiveRecord component to handle this dynamically.

		$model->phone = NULL !== $model->phone ? $model->phone : new Phone;

		$model->address = NULL !== $model->address ? $model->address : new Address;

			

		return $model;

	}



But that is pretty dissatisfying, due to the potential for error, forgetfulness, etc.

Any thoughts on how you’d handle a situation in which you needed to get either the existing Company or create a new Company, if it doesn’t exist yet, so that you can update the User record and add or update the User’s Company info at the same time? I’d like something simple and dynamic, naturally. Perhaps something that overrides whatever would return each of these relations that can include code to return a newly instantiated object if there are no records in the db.

Thanks in advance for any suggestions!

Hollyii