Many to Many

You can add a safe validator (or some other validator) for serviceIds




  public $serviceIds = array();


  public function afterFind()

  {

    ...

  }

  ...

  public function rules()

  {

    return array(

      ...

      array('serviceIds', 'safe'),

    );

  }



then change the controller code to:




if (isset($_POST['Portfolio']))

{

  $model->attributes = $_POST['Portfolio'];

  $model->services = $model->serviceIds;

  if ($model->save())

    $this->redirect(array('portfolio/admin'));

}



/Tommy

Works like a charm! Thanks!!!!!

Portfolio Model




    public $serviceIds = array();


    public function afterFind()

    {

        if (!empty($this->services))

        {

            foreach ($this->services as $n => $service)

                $this->serviceIds[] = $service->id;

        }


        parent::afterFind();

    }


    public function rules()

    {

        // NOTE: you should only define rules for those attributes that

        // will receive user inputs.

        return array(

            array('status, description, title, url, services', 'required'),

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

            array('title, url', 'length', 'max' => 255),

            array('serviceIds', 'safe'),

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

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

            array('id, status, description, title, url', 'safe', 'on' => 'search'),

        );

    }



Portfolio Controller




if (isset($_POST['Portfolio']))

        {  

            $model->attributes = $_POST['Portfolio'];

            $model->services = $model->serviceIds;

            

            if ($model->save())

                $this->redirect(array('portfolio/admin'));

        }



_form View




<div class="row oneLineLabel">

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

        <?php echo $form->checkBoxList($model, 'serviceIds',

            CHtml::listData(Service::model()->findAll(), 'id', 'name'),

            array('checkAll' => 'Check All')); ?>

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

    </div>



Much appreciated!

Matt

Thank you a lot for this elegant solution!

I need to do similar thing to that u have described in the first post, and found this solution in Google. It saved me a lot of time.

This post was execellent because I was grinding my teeth over the many to many relationship for checkboxes and saving the form data and this post just solved in 15min.

I’m not sure how this exactly works but it works.

Great job and great sharing community!!!

Hey! I’m new to PHP but already love this framework.

I have to do practically the same implementation of this example, a Restaurant can have many Services, and viceversa.

However, for some reason, the application is saving the Restaurant data, but not the Services. It doesn’t throw any error, and doesn’t seems to be entering the afterFind() function.

Any ideas why could this be happening?

The afterFind() function should be called automatically, right?

Many thanks in advance for your help!

Esteban.

Hi ecairol,

There could be a few reasons for that.

[list=1]

[*]Have you installed CAdvancedArBehavior?




    public function behaviors()

    {

        return array(

            'CAdvancedArBehavior' => array(

                'class' => Yii::getPathOfAlias('behaviors') . '.CAdvancedArBehavior',),

    }



[*]Are your relations setup correctly?

Portfolio:




    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(

            'services' => array(self::MANY_MANY, 'Service', 'tbl_portfolio_service(portfolio_id, service_id)'),

        );

    }



Service:




    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(

            'portfolios' => array(self::MANY_MANY, 'Portfolio', 'tbl_portfolio_service(service_id, portfolio_id)'),

        );

    }



[/list]

Matt

Haha, that was it Matt. I’ve just installed the extension and it worked.

Sorry, I’m pretty new to this, but thanks a lot for your help!!

Esteban,

By the way, how would you do if the Many-to-Many table (in this case PortfolioService) had another column besides the two primary keys?

When would you made the INSERT for this third column, on the Controller (just after the $model.save()) or the Model (inside the afterFind() function)?

Good question, not sure; I haven’t needed that yet. I will take a look soon and get back to you.

Matt

Regarding extra columns in the mn-table:

The best solution I found was to make a model for the mn-table as well and setup a HAS_MANY relation in the main model. Then you’ll have to use a behaviour that handles saving of HAS_MANY relations.

Also checkout my new extension, may be it helps: http://www.yiiframework.com/extension/esaverelatedbehavior/

For the extra attribute I am using a different approach for performance reasons, so that the ids are only setup when the attribute is actually accessed. Using afterFind always triggers an extra database query.

Here you go:




public $_serviceIds = null;

public function getServiceIds() {

    if($this->_serviceIds === null) {

        $this->_serviceIds = array();

        if(!$this->isNewRecord) {

            foreach($this->services as $service)

                $this->_serviceIds[]=$service->primaryKey;

        }

    }

    return $this->_serviceIds;

}

public function setServiceIds($value) {

    $this->_serviceIds = $value;

}



Hello Matt, thanks for this post, I’ve lost hours trying to make these relations work until I found it.

But now the ids of selected checkboxes are not being sent to the create action in the controller, i already checked my relations and installed CAdvancedArBehavior. Anything else I should check?

Thanks in advance!

In my case:

Artist




'events'=>array(self::MANY_MANY, 'Event', 'event_artist(artist_id, event_id)'),



Event




'artists'=>array(self::MANY_MANY, 'Artist', 'event_artist(event_id, artist_id)'),



Behaviour




return array( 

			'CAdvancedArBehavior' => array(

				'class' => 'application.extensions.CAdvancedArBehavior'));



_view




<div class="row">

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

	<?php echo $form->checkBoxList($model, 'eventsIds', CHtml::listData(Event::model()->findAll(), 'id', 'name'), array('checkAll' => 'Check All')); ?>

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

</div>



HTML output




<input id="Artist_eventsIds_1" type="checkbox" name="Artist[eventsIds][]" value="13">



Controller




$model->attributes = $_POST['Artist'];

$model->image=CUploadedFile::getInstance($model,'image');

$model->events = $model->eventsIds;

			

Yii::log('##### Selected events ' . $model->eventsIds); // this is empty



Have you setup your model with the following?




    public $serviceIds = array();


    public function afterFind()

    {

        if (!empty($this->services))

        {

            foreach ($this->services as $n => $service)

                $this->serviceIds[] = $service->id;

        }


        parent::afterFind();

    }


    public function rules()

    {

        // NOTE: you should only define rules for those attributes that

        // will receive user inputs.

        return array(

            array('serviceIds', 'safe'),

        );

    }



Thank you so much! Don’t you think to make a short tutorial? It’ll be easier to understand newbies like me

Seems to work quite well, with one major problem;

Let’s say you want to be able to generate a checkBoxList(or any selectable list) on your services form as well as the Portofolio form.

the code for this would look something like this:

Service Model:




    public $portofolioIds = array();


    public function afterFind()

    {

        if (!empty($this->portofolios))

        {

            foreach ($this->portofolios as $n => $portofolio)

                $this->potofolioIds[] = $portofolio->id;

        }


        parent::afterFind();

    }


    public function rules()

    {

        // NOTE: you should only define rules for those attributes that

        // will receive user inputs.

        return array(

            array('status, description, title, url, services', 'required'),

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

            array('title, url', 'length', 'max' => 255),

            array('portofolioIds', 'safe'),

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

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

            array('id, status, description, title, url', 'safe', 'on' => 'search'),

        );

    }



Service Controller:




if (isset($_POST['Service']))

        {  

            $model->attributes = $_POST['Service'];

            $model->services = $model->portofolioIds;

            

            if ($model->save())

                $this->redirect(array('service/admin'));

        }



And finally the _form View




<div class="row oneLineLabel">

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

        <?php echo $form->checkBoxList($model, 'portofolioIds',

            CHtml::listData(Portofolio::model()->findAll(), 'id', 'name'),

            array('checkAll' => 'Check All')); ?>

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

    </div>



The problem seems that this generates an infinite loop of afterFind functions, how would one go about resolving this.

Take into account that I am not an expert with yii.

This code works like a charm …!

But during edit how do i remain old checkboxes remain selected??

Perfect!!!

Works like a charm! Thanks!!

Hi,

This is one of the life savers.

Is it possible to extend this solution for cgridview?

I want to display more than one columns. And allow user to mark check boxes from filtered list.

Any help is highly appreciated.

Dear Friend

At the outset, I want to stress that in this thread people have used checkbox list to choose data from the related table.That means that they are deaing with minimal set of records.

In the following implementation, I used gridview to pick the records. I also disabled pagination.

With pagination, I am finding difficult to get the things done.That means that it is useful in situations where

we have minimal set of relational records.

The scenario:

I have three models representing many_many relation.

1.Meeting(table:meeting fields:id,name)

2.Member(table:member fields:id,name,age,sex,qulification)

3.MeetingMember(table:meeting_member fields:id,meeting_id,member_id).

Each meeting is attended by many members. We are taking the attendance and storing it in join table(meeting_member).

MeetingController.php




public function actionCreate()

	{

		$model=new Meeting;

                $member=new Member('search');

		$member->unsetAttributes();  

		if(isset($_GET['Member']))

			$member->attributes=$_GET['Member'];

		

		if(isset($_POST['Meeting']))

		{

			$model->attributes=$_POST['Meeting'];

			

			if($model->save())

			{

				if(isset($_POST['member']) && $_POST['member']!=="" )

				{	$members=explode(",",$_POST['member']);

					

					foreach($members as $member)

					{

						$meme=new MeetingMember;

						$meme->meeting_id=$model->id;

						$meme->member_id=$member;

						$meme->save(false);

					}

				}				

				$this->redirect(array('view','id'=>$model->id));

			}

		}


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

			'model'=>$model,

			'member'=>$member,

		));

	}




public function actionUpdate($id)

	{

		$model=$this->loadModel($id);

		$member=new Member('search');

		$member->unsetAttributes();  

		if(isset($_GET['Member']))

			$member->attributes=$_GET['Member'];


		

		if(isset($_POST['Meeting']))

		{

			$model->attributes=$_POST['Meeting'];

			if($model->save())

			{	if(isset($_POST['member']) && $_POST['member']!=="" )

				{	$members=explode(",",$_POST['member']);


				MeetingMember::model()->deleteAll("meeting_id=:mid",array(":mid"=>$model->id));                                              

					foreach($members as $member)

					{

						$meme=new MeetingMember;

						$meme->meeting_id=$model->id;

						$meme->member_id=$member;

						$meme->save(false);

					}

				}		

			

				$this->redirect(array('view','id'=>$model->id));

			}

		}


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

			'model'=>$model,

			'member'=>$member,

		));

	}




in both create.php and update.php the following modifications done.




php echo $this->renderPartial('_form', array('model'=>$model,'member'=>$member));//$model and $member is carried to the view. 



In the view we are placing the grid inside the form,the grid has checkbox column.when user checks the rows,

primary key values of member records are placed as a string inside a text field. For that we have registered a script.When we are submitting the form, string of ids is converted to array in controller.

views/meeting/_form.php




<div class="form">


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

	'id'=>'meeting-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,'name'); ?>

		<?php echo $form->textField($model,'name',array('size'=>60,'maxlength'=>64)); ?>

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

	</div>

<?php $this->widget('zii.widgets.grid.CGridView', array(

	'id'=>'member-grid',

	'dataProvider'=>$member->search(),

	'filter'=>$member,

	'enablePagination'=>false,

	'columns'=>array(

		'id',

		'name',

		'age',

		'email',

		'qualification',

		array(

		'class'=>'CCheckBoxColumn',

		'selectableRows'=>2, //enabling multiselect option

		'checked'=>function($data,$row)use($model){return in_array($data->id,$model->participants);},

		),//this ensures that during update corresponding records get checked.

		array(

			'class'=>'CButtonColumn',

		),

	),

)); ?>


<!--placing a textfield to collect the ids from member-grid-->

<?php echo CHtml::encode('members');echo "</br>";?>

<?php echo CHtml::textField('member','',array('id'=>'member'));?>

	<div class="row buttons">

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

	</div>


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


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

<?php

Yii::app()->clientScript->registerScript('collectMember','

$("#member").val('.CJavaScript::encode($model->participants).');

$("body").on("change","input[type=\"checkbox\"]",function(){


	var value=$("#member-grid").yiiGridView("getChecked","member-grid_c5");

	

	$("#member").val(value);

	});




');

/**

 *member-grid_c5 indicates that the checkBoxColumn is in the sixth column of the grid.

 */



The following things in the model, ensures that during the update the virtual property participant gets filled up.It helps in updating the checkboxes in the member grid.

Model(Meeting.php)




public $participants=array();


public function afterfind()

	{

		$this->participants=Yii::app()->db->createCommand()

			->select("member_id")

			->from("meeting_member")

			->where("meeting_id=".$this->id)

			->queryColumn();

			

		return parent::afterFind();

	}



Model(Member.php)




public function search()

{

	$criteria=new CDbCriteria;

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

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

	$criteria->compare('age',$this->age);

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

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


	return new CActiveDataProvider($this, array(

			'criteria'=>$criteria,

			'pagination'=>false,// disabled the pagination.

		));

}



I hope this would help you a bit.

The problem is here is that by disabling pagination we can not deal with large set of data.

Regards.

Dear Seenivsan,

Thank you very much for such a detailed guide. It contains tricks to use in some other places also.

Isn’t this solution work for php 5.2?

The line,


'checked'=>function($data,$row)use($model){return in_array($data->id,$model->participants);},

gave me an error.

Netbeans says;

"Language feature not compatible with PHP version indicated in project settings

Variable $row seems to be unused in its scope "

Thank you again for the help.