Recently i have been playing with Yii to get something up for someone. However, i came into a lot of problem because i am very new to Yii framework. Hence, there is a lot of reading and try and error method for me. I faced a problem where i need to populate 2 model information into 1 view. However, the CRUD that produce the codes seems to only cater for 1 to 1 sort of relationship. Furthermore, there wasn't any good information on how to tackle this problem other than the one shown on the cook book. On the cook book, it is demonstrating a 1 to 1 model relationship. In my case, it is a 1 to many relationship. What should i do?
1 to many relationship model
The main problem that i'm facing was the 1 to many relationship between my model. I have a database structured similar to the one shown on my previous article, relational active record tutorial. The scheme is shown below,
CREATE TABLE IF NOT EXISTS `invoice` (
`invoiceId` MEDIUMINT UNSIGNED NOT NULL,
`invoiceTotalAmount` DOUBLE NOT NULL,
PRIMARY KEY (`invoiceId`)
)
CREATE TABLE IF NOT EXISTS `invoice_item` (
`invoiceItemId` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT,
`invoiceItemLineId` MEDIUMINT UNSIGNED NOT NULL,
`invoiceId` MEDIUMINT UNSIGNED NOT NULL COMMENT "CONSTRAINT FOREIGN KEY (invoiceId) REFERENCES be_invoice(invoiceId)",
PRIMARY KEY (`invoiceItemId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
In my situation, i have 1 invoice having multiple line of items. So how do i show it out on my invoice view and manage my InvoiceItem and Invoice model with 1 Invoice controller?
Requirement
You should have the following files for a 1 to many relationship model to populate on 1 view.
- CRUD Invoice
- Model InvoiceItem
That's it!
How to create a 1 to many relationship model to populate into 1 view
Firstly, you will have to setup the relationship between Invoice and InvoiceItem as shown on my previous tutorial. Once, you have done that, you shouldn't touch any of your models. The only thing you should be looking at would be the controller and view created by the Invoice CRUD.
Controller Setup
Majority settings will be done on the controller. Hence, it will be quite a challenge for me to explain it clearly without dumping you too much code. Therefore, i will only be focusing on the method actionUpdate which is the update page controller method. You should have a default controller method which only cater for 1 to 1 model controller method as shown below,
public function actionUpdate()
{
$model=$this->loadModel();
// Uncomment the following line if AJAX validation is needed
// $this->performAjaxValidation($model);
if(isset($_POST['Invoice']))
{
$model->attributes=$_POST['Invoice'];
if($model->save())
$this->redirect(array('view','id'=>$model->invoiceItemId));
}
$this->render('update',array(
'model'=>$model,
));
}
Right. At this point, this is not something we want. Hence, we will have to modified it to handle 1 to many relationship models.
public function actionUpdate()
{
$Invoice=$this->loadModel();
$InvoiceItem=$this->loadManyModel($Invoice);
// Uncomment the following line if AJAX validation is needed
// $this->performAjaxValidation($model);
if(isset($_POST['Invoice']) && isset($_POST['InvoiceItem']))
{
$Invoice->attributes=$_POST['Invoice'];
$valid=$Invoice->validate();
$i = 0;
$InvoiceItemLine = new InvoiceItem;
foreach($_POST['InvoiceItem'] as $item){
if(isset($InvoiceItem[$i]))
$InvoiceItemLine = $InvoiceItem[$i];
$InvoiceItemLine->attributes= $item;
$valid=$InvoiceItemLine->validate() && $valid;
if(!$valid){
$valid = false;
}
$i++;
}
if($Invoice->save() && $valid){
$i = 0;
foreach($_POST['InvoiceItem'] as $item){
if(isset($InvoiceItem[$i]))
$InvoiceItemLine = $InvoiceItem[$i];
$InvoiceItemLine->save();
$i++;
}
$this->redirect(array('view','id'=>$Invoice->invoiceId));
}
}
$this->render('update',array(
'Invoice'=>$Invoice,
'InvoiceItem'=>$InvoiceItem,
));
}
You will notice that the above method has been modified significantly to cater for 1 to many relationship. In this method, there is one new method named 'loadManyModel'. This method basically uses the lazy loading approach to get the many relationship model record into display and is shown below,
public function loadManyModel($model)
{
if($this->_models===null)
{
if(isset($_GET['id']))
$this->_models=$model->invoiceItems;
if($this->_models===null)
throw new CHttpException(404,'The requested page does not exist.');
}
return $this->_models;
}
The method will required to take in the loadModel return value which is the single relationship model object so that we can use the lazy loading approach. If you have no idea what i am talking about, please read my previous tutorial before coming here. This is being done this way to improve the efficiency and reduce the number of SQL query being called by using the eager loading approach. (take note that there is a new global variables called $_models which you would have to declared next to the global variables $_model)
I'm pretty lazy to explain what is the actionUpdate method is trying to do. But i will explain the concept behind this which it probably makes more sense. In my case, there are two model where Invoice only has 1 records but InvoiceItem would have more than 1 records. By using the loadManyModel, i can retrieved these many records object into a variables and passed it into my view for display. Once, the user hits submit, i will have to loop through the items that has been submitted for those records that are in InvoiceItem and perform the same verification and methods as a single record by looping each individual records in InvoiceItem. Just that simple 🙂
Setting up the view
If you think you could use the variables that you just passed through the controller, by editing _form.php, you might be wrong. You would also required to update the corresponding method view files to take in the new variables as shown below,
<?php
$this->breadcrumbs=array(
'Invoices'=>array('index'),
$Invoice->invoiceId=>array('view','id'=>$Invoice->invoiceId),
'Update',
);
$this->menu=array(
array('label'=>'List Invoice', 'url'=>array('index')),
array('label'=>'Create Invoice', 'url'=>array('create')),
array('label'=>'View Invoice', 'url'=>array('view', 'id'=>$Invoice->invoiceId)),
array('label'=>'Manage Invoice', 'url'=>array('admin')),
);
?>
<h1>Update Invoice <?php echo $Invoice->invoiceId; ?></h1>
<?php echo $this->renderPartial('_form', array('Invoice'=>$Invoice, 'InvoiceItem'=>$InvoiceItem)); ?>
Btw, the above code is took from my view/update.php file where you see at the last sentence i passed in the variables that i have given through my controller. Once you have setup the view, you should be able to used it on the view/_form.php file. In this file, you should only see the code generated for Invoice class. All you have to do is to loop through the InvoiceItem variables that you have just passed through the controller and display it out as shown below,
<?php echo $form->errorSummary($InvoiceItem); ?>
<?php
foreach($InvoiceItem as $item){
?>
<div class="row">
<?php echo $form->labelEx($item,'['.$item->invoiceItemLineId.']invoiceItemLineId'); ?>
<?php echo $form->textField($item,'['.$item->invoiceItemLineId.']invoiceItemLineId' ); ?>
<?php echo $form->error($item,'['.$item->invoiceItemLineId.']invoiceItemLineId'); ?>
</div>
<?php } ?>
Just that simple! And i spend a few days to figure this out! hahaha! This way, you can easily populate 2 modle information using just 1 controller and 1 view. In other words, 1 CRUD and 1 Model. Hope this helps someone else out 🙂