CakePHP 3.x - Save many-to-many data - cakephp

I am attempting to patchEntity with a join table, but I am unable to get it to save the associated records, and I think I have the backend code correct, but I am unsure of what the frontend code should look like...
Here is the scenario, I have three tables
Ledgers Table
id int
title string
Tribes Table
id int
name string
LedgersTribes Table
id int
ledger_id int
tribe_id int
Here is my backend code
public function ledgerSave($id = null)
{
$this->loadModel('Ledgers');
$ledger = $this->Ledgers->get($id, [
'contain' => ['LedgersTribes']
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$ledger = $this->Ledgers->patchEntity($ledger, $this->request->getData(), [
'associated' => ['LedgersTribes']
]);
if ($this->Ledgers->save($ledger)) {
$this->Flash->success(__('The ledger has been saved.'));
return $this->redirect($this->referer());
}
$this->Flash->error(__('The ledger could not be saved. Please, try again.'));
}
$this->set(compact('ledger'));
}
Here is the relevant frontend code
<?= $this->Form->control('tribe_id[]', ['type' => 'checkbox', 'label' => false, 'class' => 'minimal', 'value' => $tribe->id]) ?>
My question is, what should the field name be for the tribe_id, the idea is, i have a list of checkboxes and the user checks off a couple of boxes and then those tribe_id's get inserted into the LedgersTribes table with the ledger_id
Any ideas on how I can do this?
EDIT: Here is a screenshot of the form
I have reviewed the following links, and none of them answer my question...
CakePHP 3: Save Associated Model Fail
Save associated in cakephp 3 not working
How to save associated joinData in cakephp 3.x
CakePHP 3 cannot save associated model
Cakephp 3 - Save associated belongsToMany (joinTable)

This should do:
echo $this->Form->control('tribes._ids[]', [
'type' => 'checkbox',
'label' => false,
'class' => 'minimal',
'value' => $tribe->id]
]);
This is described here: https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-inputs-for-associated-data

I think you have to get the table (ex: TableRegistry::getTableLocator()->get('')) where you want to save the data. Then create entity from that and save the data, hopefully it will work.

Related

CakePHP Save newly created ID in Parent ID field of self assoc model

I have a parent/child self association table where when the id = parent_id, that id is it's own parent. However I'm having trouble saving data into my table from the add action/view
From add.ctp view - when adding a new record, I select a parent_id from the drop down box and enter a name.
<?php
echo $this->Form->input('parent_id', array('empty' => 'No Parent'));
echo $this->Form->input('name');
?>
If user selects "No Parent" this means I would like the parent_id = id where id is the unique ID automatically created in DB at time it is saved.
This is what is passed into $this->request->data when 'No Parent' is selected.
array(
'Item' => array(
'parent_id' => '',
'name' => 'testname'
)
)
I have tried to set the parent_id = id in the beforeSave but since id does not yet exist, there is nothing to assign parent_id to. I have also tried calling the "parent" model save first and saveAll in the controller but those don't work either.
Controller
public function add() {
if ($this->request->is('post')) {
$this->Item->create();
//have tried calling parent model in self association first but
//if ($this->Item->ParentItem->save($this->request->data)) {
if ($this->Item->saveAll($this->request->data)) {
$this->Session->setFlash(__('The item has been saved'));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(__('The item could not be saved. Please, try again.'));
}
}
Item.php / Model relationship
public $belongsTo = array(
'ParentItem' => array(
'className' => 'Item',
'foreignKey' => 'parent_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
public $hasMany = array(
'ChildItem' => array(
'className' => 'Item',
'foreignKey' => 'parent_id',
'dependent' => false,
'conditions' => '',
'fields' => '',
'order' => '',
)
);
How can I take an ID that is just created/saved and save that to another field, in this case parent_id?
UPDATE:
I have been working on this some more and I have used getInsertId() to get the last inserted Id and I am trying to save that into the parent_id, but there is something that prevents this. I have removed all model validation to make sure it wasn't that. But is there something in Cake (or my association setup) that does not allow parent_id = id (i.e. a row is it's own parent?
This is the latest code in my add action... This saves a row to the DB, but w/o a parent_id. I then try to edit using the add action and set the parent_id = id, but even the edit wont allow a save.
public function add() {
if ($this->request->is('post')) {
$this->Item->create();
if ($this->Item->save($this->request->data)) {
$last_id = $this->Item->getInsertId();
$this->Item->saveField('parent_id', $last_id);
$this->Session->setFlash(__('The item has been saved'));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(__('The item could not be saved. Please, try again.'));
}
}
I have also tried calling $this->Item->ParentItem->save(), saveAll, 'deep' => true, but still nothing is allowing me to update the parent_id column. A row ge
Thanks in advance
In Your model of association You can set foregin_key to parent_id, and will be automatic filled
So the problem I was having... trying to set a parent_id = id (making id it's own parent basically) in a self join model is due to this piece of code in my Model file
public $actsAs = array('Tree');
After reading through the Tree Behavior again, I realized that $this->Model->save (or saveField) does not really work well with Tree structures and updating parent IDs. Should use the behaviors functions instead form my understanding. Also the TreeBehavior expects some parent_ids to be null (at least top level parent_ids), so if I were to leave this as a tree, the ids with a null parent_id would be considered the parent.
http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html
"The parent field must be able to have a NULL value! It might seem to work if you just give the top elements a parent value of zero, but reordering the tree (and possible other operations) will fail."
So I need to decide if I want to use tree behavior or have simple parent/child relationship without using Tree... think I will do the later as I don't have a need for multilevel tree, just a parent and one level of children.
Thanks for replies.

CakePHP HABTM Form Submission

I have two tables, questions and tags, that have a HABTM relationship. When adding a question, I want to be able to specify a tag for the question (this would just be the first tag, with ability to add more tags later). The tags are pulled from their table. How can I configure my application so that when a question is added and a tag is specified, the join is reflected in the join table (questions_tags)?
Here is my question add action code :
function add() {
$tags = $this->Question->Tag->find('all');
$this->set('tags',$tags);
if (!empty($this->data)) {
$this->Question->create();
if ($this->Question->save($this->data)) {
$this->Session->setFlash(__('The question has been saved', true));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(__('The question could not be saved. Please, try again.', true));
}
}
$users = $this->Question->User->find('list');
$tags = $this->Question->Tag->find('list');
$this->set(compact('users', 'tags'));
}
and here is my question add view code:
<?php
echo $this->Form->create('Question');
echo $this->Form->input('user_id',array('type' => 'hidden', 'value' => $this->Session->read('Auth.User.id')));
echo $this->Form->input('title');
echo $this->Form->input('details',array('type' => 'textarea'));
echo $this->Form->input('tag_id');
echo $this->Form->end(__('Submit', true));
?>
First make sure that your models are set up the right way. The fact that a user initially just adds one tag to your question does not change the fact that you should have a HABTM relation between the Question model and the Tag model (because you want the possibility add more tags later).
If your $this->data array is build according to the following schema:
$this->data = array(
'Question' => array(
'name' => 'Trick question'
),
'Tag' => array(
'Tag' => array(1,2,3)
)
);
Then a $this->Question->save() will save the Question data as well as the related Tag data (in this case Question 'Trick Question' with Tags with the id 1, 2 and 3).
Maybe take one step back and bake your Models, Views and Controllers for these two models (again) and see what Cake makes out of it. If I'm correct you'll just need a $this->Form->input('Tag') somewhere in your form (and if that not fills in the right data automatically you want to fill the options parameter with the result of $this->Question->Tag->find('list')).
if you have a single tag for a question, it's not HABTM. it has to be one to one or one to many relation.
in your questions model you can define property belongsTo:
class Question extends AppModel {
var $name = 'Question';
var $belongsTo = array(
'Tag' => array(
'className' => 'Tag',
'foreignKey' => 'tag_id'
)
);
}
Something like this.
here is a link describing how to set HABTM
HABTM

Refill relationships in form in cakephp in edit mode

I got a controller named Posts, a model called Content which is properly linked with other models such as Category and Location.
In my view for add 'Content' i successfully populate the multi select lists with categories and locations to pick to relate to the post. Saving it all works perfectly.
Now in edit/update mode, I can once again fill the multi selects with categories and locations, but it will not select the ones related to the current post. When looking in the database, there are categories and locations successfully realted to the current post.
This is what I got in my controller:
$this->data = $this->Content->read();
$this->set('locations',$this->Content->Location->find('list',array('fields' => array('id','location'))));
$this->set('categories',$this->Content->Category->find('list',array('fields' => array('id','category'))));
And this is what I got in my view:
echo $this->Form->input('Location', array('type' => 'select','multiple' => 'true','options' => $locations));
echo $this->Form->input('Category', array('type' => 'select','multiple' => 'true','options' => $categories));
What am i missing here? How do i get the already related locations and categories, select in the multi select lists?
(filling of non relationship data, will repopulate textfields etc just perfectly)
Grateful for any help!
Jason
instead of
$this->data = $this->Content->read()
try
$params['conditions'] = array(
'Content.id' => $id
);
$params['contain'] = array(
'Category',
'Location'
);
$this->data = $this->Content->find('first', $params);
You will need the Containable Behaviour for that
Use this:
echo $this->Form->input('Location', array(
'label' => 'Location',
'type' => 'select',
'options' => $LocationArray,
'selected'=> 12(Selected Value)
);

how to create a chain select form in cakephp

My business directory application calls for 3 chained select boxes, and I'm using cakephp to build this application.
The hierarchy and order of choices for the sections is this:
1 - business group
2 - business type
3 - city (included in table customer)
The relationships are:
customer HABTM business types
business groups have many business types
business types have one business group, HABTM customers
I have searched for jquery plugins that help with this, and found one by Remy Sharp, but it doesn't have the more complex relationships I have.
http://remysharp.com/2007/09/18/auto-populate-multiple-select-boxes/
What I imagine happening is the first selection box (business groups) is pre-populated and once a selection is made, an event listener send a message that filters the second selection box, and the same for the third.
What I don't know is how to structure the search action based on the event listener.
Any advice or am I way off base?
As always, I come to the well for help.
Much appreciated.
Paul
Thanks very much Nick, I've read many of your posts I really appreciate your response.
I've followed your instructions but have run into problems. I've tried my best to resolve them but haven't figured it out.
This is what I've done so far:
1) created 'chained' actions in both the business_type and business_directory (renamed customer to business directory, which is more appropriate.)
business type chained action:
function chained($business_group_id) {
$business_types = $this->BusinessType->find('list', array(
'conditions' => array( 'BusinessType.business_group_id' => $business_group_id)
));
$this->set('business_types', $business_types);
}
business directory chained action:
function chained($business_type_id) {
$business_directories = $this->BusinessDirectory->bindModel(array( 'hasOne' => array('business_directories_business_types' )));
$business_directories = $this->BusinessDirectory->find('all', array(
'fields' => array( ' BusinessDirectory.city'),
'conditions' => array( 'business_directories_business_types.business_type_id' => $business_type_id)
));
$this->set('business_directories', $business_directories);
}
I did find that with a HABTM relationship, using find 'list' didn't create the join query, whereas find 'all' did.
2) I then created a search action in the business directory and corresponding view.
For the business groups I created a getList action to populate the option list in the search form:
function getList() {
return $this->BusinessGroup->find('list');
}
In the search view, I've added the javascript for the chain select:
<script type="text/javascript">
<!--
$(function () {
var group = $('#businessGoup');
var type = $('#businessType');
var city = $('#businessDirectoryCity');
type.selectChain({
target: city,
url: '../business_directories/chained/'+$(this).val(),
data: { ajax: true, anotherval: "anotherAction" }
});
group.selectChain({
target: type,
url: '../business_types/chained/'+$(this).val()
}).trigger('change');
});
//-->
</script>
And the form:
create('business_directories', array('action'=>'/search_results')); ?>
input('business_group_id',
array( 'type' => 'select',
'id' => 'businessGoup',
'empty' => '-- Select Business Group --',
'multiple' => true,
'options' => $this->requestAction('/business_groups/getList' ),
'label' => 'Business Group'));
?>
input('business_type.id',
array( 'type' => 'select',
'id' => 'businessType',
'empty' => '-- Select Business Type --',
'multiple' => true,
'options' => 'none selected',
'label' => 'Business Type'));
?>
input('business_directories.id',
array( 'type' => 'select',
'id' => 'businessDirectoryCity',
'empty' => '-- Select City --',
'multiple' => true,
'options' => 'options',
'label' => 'City'));
?>
end('Search'); ?>
When I test the business type chain function, /business_types/chained/1, everything works.
But when I test the search view, I get a javascript alert error. Then when I check firebug, I get the following two errors:
Warning (2): Missing argument 1 for BusinessTypesController::chained() [APP\controllers\business_types_controller.php, line 71]
Notice (8): Undefined variable: business_group_id [APP\controllers\business_types_controller.php, line 73]
Any additional help with this is very much appreciated.
Thanks, Paul
What you need is to have 2 actions in the controllers (business_type and customer).
each action should look like this. In that case for the business type
function chained($parent_id){
$business_types = $this->BusinessType->find('list', array('conditions'=>'BusinessType.business_group_id'=>$parent_id));
$this->set('business_types', $business_types);
}
of course you need also view for that action which will format the values in the proper format for the chained select.
For Business group you need to show all values directly so no ajax is needed.
The Customer controller's action is similar, but you need to select cities of all related customers.
Then with the chained select you need to set the proper elements and set the proper actions which need to be called.
i.e.:
$('#id-of-the-business-group').selectChain({
target: $('#id-of-the-business-type-field'),
url: '/business_types/chained/'+$(this).val()
});

Edit of self referencing HABTM in cakephp works, sometimes

I'm using a self referencing HABTM model with Participants. You sign up for an event and when you log in to your reservation/profile you see a list of other participants and you can choose to add yourself and others into various groups; share hotel room, share transportation from airport etc.
What I've managed so far:
1) In my profile I see the list of all other participants with checkboxes. Great so far.
2) Adding another participant works fine. Next time I edit, the participant I added is shown as checked.
3) Removing another participant works fine too as long as you still have checked participants before you submit!
Again, with words:
There are 3 participants. I'm logged in as one of them, and I see the 2 other people on the participants list. I choose to check both of them. This works fine (always). Later I choose to remove one of them (by unchecking the checkbox and hitting submit). This also works fine (always). If I want to remove the last checkbox... nothing is updated (always!). What's curious is that I can add and remove any odd combination of participants and it will always work UNLESS I choose to remove every participants in one go (removing a one and only participant is a special case of "remove all checked participants").
As far as I know, HABTMs work by first deleting all relations, then re-saving them. I can see that in my tables when I remove, add, remove, add the same participant over and over again - the id on the HABTM table is always increasing. When I deselect all participants at once, however, the relations are not updated. The ids stay the same, so it's like the save never happened.
This behaviour is so specific and peculiar, I have a feeling I'm missing something obvious here. Anyway, here's the relevant code:
Model
class Participant extends AppModel {
var $hasAndBelongsToMany = array(
'buddy' => array(
'className' => 'Participant',
'joinTable' => 'participants_participants',
'foreignKey' => 'participant_id',
'associationForeignKey' => 'buddy_id',
'unique' => true,
)
);
Controller
function edit($id = null) {
if (!$id && empty($this->data)) {
$this->Session->setFlash(__('Invalid Participant', true));
$this->redirect(array('action'=>'index'));
}
if (!empty($this->data)) {
if ($this->Participant->saveAll($this->data)) {
$this->Session->setFlash(__('The Participant has been saved', true));
$this->redirect(array('action'=>'index'));
} else {
$this->Session->setFlash(__('The Participant could not be saved. Please, try again.', true));
}
}
if (empty($this->data)) {
$this->data = $this->Participant->read(null, $id);
}
// Fetching all participants except yourself
$allParticipants = $this->Participant->find('list', array('conditions' => array('participant.id ' => $id)));
// Fetching every participant that has added you to their list
$allBuddies = $this->Participant->ParticipantsParticipant->find('list', array(
'conditions' => array('buddy_id' => $id),
'fields' => 'ParticipantsParticipant.participant_id',
'order' => 'ParticipantsParticipant.participant_id ASC'
));
$this->set(compact('allParticipants','allBuddies'));
}
View
echo $form->create('Participant');
echo $associations->habtmCheckBoxes($allParticipants, $this->data['buddy'], 'buddy', 'div', '\'border: 1px solid #000;\'', '\'border: 1px solid #000;\'');
echo $form->end('Submit');
I'm using a slightly modified helper, habtmCheckBoxes, found here: http://cakeforge.org/snippet/detail.php?type=snippet&id=190
It works like this: function habtmCheckBoxes($rows=array(), $selectedArr=array(), $modelName, $wrapTag='p', $checkedDiv, $uncheckedDiv) {}
This happens because of the way HABTM works within CakePHP - it will only save HABTM data if the HABTM key exists in the array. Hence, when no checkboxes are checked, there is no data passed through and cake doesn't touch your existing habtm records.
A quick fix would be to add a few lines of code to your controller.
if (!empty($this->data)) {
if (empty($this->data['buddy'])) {
$this->data['buddy'] = array('buddy' => array(''));
}
if ($this->Participant->saveAll($this->data)) {
// ...
} else {
// ...
}
}
However, if might also be possible for you to use cake's form helper (instead of the other helper you are using) to do this in your view:
echo $form->inputs(array(
'legend' => 'Nominate your artwork for awards',
'buddy' => array('label' => false, 'multiple' => 'checkbox', 'options' => $allBuddies)
));

Resources