Yii Framework Forum: Many to Many - Yii Framework Forum

Jump to content

  • (2 Pages)
  • +
  • 1
  • 2
  • You cannot start a new topic
  • You cannot reply to this topic

Many to Many Rate Topic: ***** 1 Votes

#1 User is offline   waterloomatt 

  • Advanced Member
  • PipPipPip
  • Yii
  • Group: Members
  • Posts: 545
  • Joined: 09-April 10

Posted 06 March 2011 - 06:01 AM

Hi,

I know this topic has been beaten to death but I still can't find a conclusive answer for dealing with many to many relationships.

I have two tables (and models) Portfolio & Service. There is a link table PortfolioService that has PortfolioId and ServiceId as PKs.

I am using the CAdvancedArBehavior behavior to handle the Many to Many saving. All is working well. But...

I am using checkBoxList to render a list of services that a user can check on the portfolio. It works well except when the validation fails for submitting the form. The checkBoxList is cleared and the posted values are not repopulated.

<?php echo $form->labelEx($model, 'services'); ?>
        <?php echo $form->checkBoxList($model, 'services',
            CHtml::listData(Service::model()->findAll(), 'id', 'name'),
            array('attributeitem' => 'id', 'checkAll' => 'Check All')); ?>
        <?php echo $form->error($model, 'services'); ?>


I have tried using Relation Widget but I don't want to use it in my app.

Question: is there a way to use checkBoxList using CAdvancedArBehavior? If not, can you please point me to a "complete" reference for manually managing the Many to Many relationships - I.e. using a 3rd Model to link the 2 parent models.

Thanks!
0

#2 User is offline   tri 

  • Elite Member
  • Yii
  • Group: Moderators
  • Posts: 1,651
  • Joined: 20-November 08
  • Location:Stockholm, Sweden

Posted 06 March 2011 - 09:46 AM

View Postwaterloomatt, on 06 March 2011 - 06:01 AM, said:

...
I am using checkBoxList to render a list of services that a user can check on the portfolio. It works well except when the validation fails for submitting the form. The checkBoxList is cleared and the posted values are not repopulated.
...


This may be the same problem you would encounter in an update action.
This post has an explanation.

In your case try this:

Portfolio.php
  public $serviceIds=array();
  ...
  public function afterFind()
  {
    if(!empty($this->services))
    {
      foreach($this->services as $n=>$service)
        $this->serviceIds[]=$service->id;
    }
  }

PortfolioController.php
  ...
  $model->services = $_POST['Portfolio']['serviceIds'];
  ...

_form.php
<?php
  echo $form->checkBoxList($model, 'serviceIds',
    CHtml::listData(Service::model()->findAll(), 'id', 'name'),
    array('attributeitem' => 'id', 'checkAll' => 'Check All'));
?>

/Tommy
Don't forget to read The Definitive Guide to Yii (en) (sv) | The class reference has the details
0

#3 User is offline   waterloomatt 

  • Advanced Member
  • PipPipPip
  • Yii
  • Group: Members
  • Posts: 545
  • Joined: 09-April 10

Posted 07 March 2011 - 12:58 AM

Hi Tommy,

Thanks. I had to play around a little to get it to work. FYI, I have to set both $model->services & $model->serviceIds in the controller. It is a little ugly and I'll be looking for a cleaner solution. I will post back anything that I find.

Portfolio Model
    public $serviceIds = array();

    public function afterFind()
    {
        if (!empty($this->services))
        {
            foreach ($this->services as $n => $service)
                $this->serviceIds[] = $service->id;
        }

        parent::afterFind();
    }


Portfolio Controller
if (isset($_POST['Portfolio']))
        {
            
            $model->attributes = $_POST['Portfolio'];
            $model->services = $_POST['Portfolio']['serviceIds'];
            $model->serviceIds = $_POST['Portfolio']['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>


Thanks,

Matt

View Posttri, on 06 March 2011 - 09:46 AM, said:

This may be the same problem you would encounter in an update action.
This post has an explanation.

In your case try this:

Portfolio.php
  public $serviceIds=array();
  ...
  public function afterFind()
  {
    if(!empty($this->services))
    {
      foreach($this->services as $n=>$service)
        $this->serviceIds[]=$service->id;
    }
  }

PortfolioController.php
  ...
  $model->services = $_POST['Portfolio']['serviceIds'];
  ...

_form.php
<?php
  echo $form->checkBoxList($model, 'serviceIds',
    CHtml::listData(Service::model()->findAll(), 'id', 'name'),
    array('attributeitem' => 'id', 'checkAll' => 'Check All'));
?>

/Tommy

0

#4 User is offline   tri 

  • Elite Member
  • Yii
  • Group: Moderators
  • Posts: 1,651
  • Joined: 20-November 08
  • Location:Stockholm, Sweden

Posted 07 March 2011 - 06:52 AM

View Postwaterloomatt, on 07 March 2011 - 12:58 AM, said:

...
Portfolio Controller
if (isset($_POST['Portfolio']))
        {
            
            $model->attributes = $_POST['Portfolio'];
            $model->services = $_POST['Portfolio']['serviceIds'];
            $model->serviceIds = $_POST['Portfolio']['serviceIds'];
            
            if ($model->save())
                $this->redirect(array('portfolio/admin'));
        }

...


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
Don't forget to read The Definitive Guide to Yii (en) (sv) | The class reference has the details
0

#5 User is offline   waterloomatt 

  • Advanced Member
  • PipPipPip
  • Yii
  • Group: Members
  • Posts: 545
  • Joined: 09-April 10

Posted 07 March 2011 - 10:57 AM

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
1

#6 User is offline   LastDay 

  • Junior Member
  • Pip
  • Yii
  • Group: Members
  • Posts: 62
  • Joined: 06-March 11
  • Location:Russia

Posted 12 March 2011 - 07:12 PM

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.
0

#7 User is offline   Thim 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 6
  • Joined: 31-December 10

Posted 10 April 2011 - 12:34 PM

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!!!
0

#8 User is offline   ecairol 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 4
  • Joined: 02-May 11

Posted 02 May 2011 - 03:36 PM

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.
0

#9 User is offline   waterloomatt 

  • Advanced Member
  • PipPipPip
  • Yii
  • Group: Members
  • Posts: 545
  • Joined: 09-April 10

Posted 02 May 2011 - 09:17 PM

Hi ecairol,

There could be a few reasons for that.

  • 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)'),
            );
        }
    


Matt
0

#10 User is offline   ecairol 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 4
  • Joined: 02-May 11

Posted 06 May 2011 - 02:37 PM

View Postwaterloomatt, on 02 May 2011 - 09:17 PM, said:

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',),
    }



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,
0

#11 User is offline   ecairol 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 4
  • Joined: 02-May 11

Posted 09 May 2011 - 08:56 PM

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)?
0

#12 User is offline   waterloomatt 

  • Advanced Member
  • PipPipPip
  • Yii
  • Group: Members
  • Posts: 545
  • Joined: 09-April 10

Posted 10 May 2011 - 02:52 AM

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

Matt

View Postecairol, on 09 May 2011 - 08:56 PM, said:

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)?

0

#13 User is offline   sluderitz 

  • Standard Member
  • PipPip
  • Yii
  • Group: Members
  • Posts: 105
  • Joined: 26-February 09
  • Location:Germany

Posted 18 June 2011 - 07:47 AM

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.
0

#14 User is offline   sluderitz 

  • Standard Member
  • PipPip
  • Yii
  • Group: Members
  • Posts: 105
  • Joined: 26-February 09
  • Location:Germany

Posted 22 June 2011 - 08:25 AM

Also checkout my new extension, may be it helps: http://www.yiiframew...elatedbehavior/
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;
}

0

#15 User is offline   Hernan Garcia 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 4
  • Joined: 16-July 11

Posted 31 July 2011 - 12:41 PM

View Postwaterloomatt, on 02 May 2011 - 09:17 PM, said:

Hi ecairol,

There could be a few reasons for that.

  • 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)'),
            );
        }
    


Matt


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

0

#16 User is offline   waterloomatt 

  • Advanced Member
  • PipPipPip
  • Yii
  • Group: Members
  • Posts: 545
  • Joined: 09-April 10

Posted 14 August 2011 - 04:02 AM

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

0

#17 User is offline   Anatoly Rugalev 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 10
  • Joined: 02-April 12
  • Location:Russia, Perm

Posted 06 April 2012 - 10:32 PM

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

This post has been edited by Anatoly Rugalev: 06 April 2012 - 10:33 PM

Peace. Love. Yii.
0

#18 User is offline   hmsdexter 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 12
  • Joined: 07-May 12

Posted 07 May 2012 - 08:11 AM

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.
0

#19 User is offline   vibhaJadwani 

  • Junior Member
  • Pip
  • Yii
  • Group: Members
  • Posts: 32
  • Joined: 13-June 12
  • Location:India

Posted 19 June 2012 - 08:45 AM

This code works like a charm ..!
But during edit how do i remain old checkboxes remain selected??
0

#20 User is offline   jairbatista81 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 3
  • Joined: 05-November 12

Posted 08 November 2012 - 09:24 PM

View Postwaterloomatt, on 07 March 2011 - 10:57 AM, said:

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


Perfect!!!
Works like a charm! Thanks!!
0

Share this topic:


  • (2 Pages)
  • +
  • 1
  • 2
  • You cannot start a new topic
  • You cannot reply to this topic

1 User(s) are reading this topic
0 members, 1 guests, 0 anonymous users