I'm trying to add associated data to during cakePHPs beforeSave() but it looks like cakePHP is ignoring it.
<?php
App::uses("AppModel", "Model");
class Recipe extends AppModel {
public $hasMany = array('Ingredient');
public function beforeSave($options=array()) {
if(!isset($this->data["Ingredient"])) {
Debugger::log("data[Ingredient] didn't exist! adding some ingredients...");
$this->data["Ingredient"] = array(
array(
'Ingredient' => array(
'name' => 'Gluten Free Flour',
'amount' => '12 cups'
)
),
array(
'Ingredient' => array(
'name' => 'Sugar',
'amount' => '50 cups'
)
),
array(
'Ingredient' => array(
'name' => 'A shoe',
'amount' => 'Just one'
)
),
);
}
Debugger::log("Saving Data: ");
Debugger::log($this->data, 7, 10);
return true;
}
}
In my RecipesController I have this:
public function test_save() {
//Test saving With the ingredients in the recipe
Debugger::log("Saving w/ ingredients");
Debugger::log($this->Recipe->saveAssociated(array(
'Recipe' => array(
'name' => 'Test Recipe 1'
),
'Ingredient' => array(
array(
'name' => 'Test Ingredient 1 for Test Recipe 1',
'amount' => 'a few'
),
array(
'name' => 'Test Ingredient 2 for Test Recipe 1',
'amount' => 'a lot'
)
)
)));
Debugger::log("Saving w/o ingredients");
Debugger::log($this->Recipe->saveAssociated(array(
'Recipe' => array(
'name' => 'Test Recipe 2'
)
)));
}
The save with ingredients works, but the save without ingredients doesn't, the data in Recipe->data before beforeSave returns is this:
array(
'Recipe' => array(
'name' => 'Test Recipe 1'
),
'Ingredient' => array(
(int) 0 => array(
'Ingredient' => array(
'name' => 'Test Ingredient 1 for Test Recipe 1',
'amount' => 'a few'
)
),
(int) 1 => array(
'Ingredient' => array(
'name' => 'Test Ingredient 2 for Test Recipe 1',
'amount' => 'a lot'
)
)
)
)
And when Ingredient is added in the before save:
array(
'Recipe' => array(
'name' => 'Test Recipe 1'
),
'Ingredient' => array(
(int) 0 => array(
'Ingredient' => array(
'name' => 'Gluten Free Flour',
'amount' => '12 cups'
)
),
(int) 1 => array(
'Ingredient' => array(
'name' => 'Sugar',
'amount' => '50 cups'
)
),
(int) 2 => array(
'Ingredient' => array(
'name' => 'A shoe',
'amount' => 'Just one'
)
)
)
)
When the data is in the array before I call a save method, it'll work just fine, only when I try and add the associated data in the beforeSave.
From the documentation:
Be sure that beforeSave() returns true, or your save is going to fail.
http://book.cakephp.org/2.0/en/models/callback-methods.html#beforesave
Edited: I tested your code and you are right, related data added in beforeSave() is being ignored. This is happening because you cannot add or modify related data in a model's beforeSave(). It means that, in your Recipe's beforeSave(), you cannot neither add Ingredients nor modify the Ingredients you previously attached to Recipe in your controller.
In your Recipe's beforeSave() you can only modify Recipe data.
Is this a bug? It is not, according to Mark Story (creator of CakePHP):
Its a feature, as currently saveAll only lets you use beforeSave to
modify the record that's about to be saved, not the full data set. I
personally don't feel that should change, but its something that could
be discussed.
This statement is about saveAll() but the important part is what he said about beforeSave(). Check the full thread: https://github.com/cakephp/cakephp/issues/1765
Related
Is it possible to retrieve records where the index is the record id?
I have a model with a hasMany Relation. So my study can have multiple texts and questions etc.
The custom retrieve function looks like this:
protected function _findStudyWithSequence($state, $query, $results=array()){
if ($state === 'before') {
$query['contain'] = array(
'SubjectFilter'=>array('fields'=>'SubjectFilter.studyCode'),
'Text'=>array('fields'=>'Text.position,Text.id,Text.text','order'=>array('position')),
'Image'=>array('fields'=>'Image.position, Image.id','order'=>array('position')),
'Video'=>array('fields'=>'Video.position, Video.id','order'=>array('position')),
'Web'=>array('fields'=>'Web.position, Web.id','order'=>array('position')),
'Likert'=>array('fields'=>'Likert.position, Likert.id','order'=>array('position')),
'Question'=>array('fields'=>'Question.position, Question.id','order'=>array('position')),
'Star'=>array('fields'=>'Star.position, Star.id','order'=>array('position')),
'Multiple_choice'=>array('fields'=>'Multiple_choice.position, Multiple_choice.id','order'=>array('position'))
);
$query['recursive']=-1;
$query['order']=array('Study.started DESC');
$query['fields']=array(
'Study.id', 'Study.user_id','Study.title','Study.description','Study.numberParticipants','Study.state','Study.created','Study.modified','Study.started','Study.completed','Study.numberParticipants');
return $query;
}
}
return $results;
}
As you can see I am using containable
The results look like this:
array(
'Study' => array(
'id' => '1845346986',
'user_id' => '1402402538',
'title' => 'bla bla',
'description' => '',
'numberParticipants' => '10',
'state' => 'active',
'created' => '2017-08-21 14:06:11',
'modified' => '2017-08-21 14:29:56',
'started' => '2017-08-21 14:30:06',
'completed' => '0000-00-00 00:00:00',
),
'SubjectFilter' => array(
'studyCode' => null
),
'Text' => array(
(int) 0 => array(
'position' => '0',
'id' => '1423909242',
'text' => '<p>Bla <b>bla </b></p><p>blub</p><p><br /></p>',
'study_id' => '1845346986'
)
),
'Question' => array(
(int) 0 => array(
'position' => '4',
'id' => '729521908',
'study_id' => '1845346986'
)
), etc
But I want the Text and Question index to be the ids. Like this:
'Text' => array(
(int) 1423909242 => array(
'position' => '0',
'id' => '1423909242',
'text' => '<p>Bla <b>bla </b></p><p>blub</p><p><br /></p>',
'study_id' => '1845346986'
)
),
How can i manage this?
You can use CakePHP's amazing Hash::combine() method.
Something like:
$results['Text'] = Hash::combine($results['Text'], '{n}.id', '{n}');
$results['Question'] = Hash::combine($results['Question'], '{n}.id', '{n}');
return $results;
It's not tested code, but I believe it's correct (or at least close), and you should get the idea from here.
Notes on Hash::combine:
Creates an associative array using a $keyPath as the path to build its
keys, and optionally $valuePath as path to get the values. If
$valuePath is not specified, or doesn’t match anything, values will be
initialized to null. You can optionally group the values by what is
obtained when following the path specified in $groupPath.
Hash::combine(array $data, $keyPath, $valuePath = null, $groupPath =
null)
I have a model called Category which hasMany Expense. I am trying to generate an accordion type interface where a user can expand a category and edit the expenses. I can generate the input fields but they don't seem to be in "edit" mode because they are not pre-populated.
I've searched online and found a few related articles such as this one CakePHP: How to update multiple records at the same time with the Form helper and also this one CakePHP: Form helper with saveMany() to edit multiple rows at once. I have tried to emulate their code but with no success.
My CategoriesController Index function looks like this ...
public function index($project_id) {
$data = $this->Category->find('all', array(
'conditions'=>array('Category.project_id' => $project_id),
'order'=>array('Category.category_name')
));
$this->set('categories', $data);
$this->request->data = $data;
}
I have read that cakephp2 requires $this->request->data to be set for FormHelper to work. But in the examples I have found online everyone seems to use the plural of the model name so I tried that as well.
My Categories\index.ctp looks like this. I'm not at the "accordion" stage yet. I'm just trying to get input boxes on the screen that are pre-populated.
$i=0;
foreach ($categories as $category)
{
echo $this->Form->input("Category.$i.category_id");
echo $this->Form->input("Category.$i.category_name");
$j=0;
foreach($category['Expense'] as $expense)
{
echo $this->Form->input('Expense.' . $j . '.expense_id');
echo $this->Form->input('Expense.' . $j . '.expense_name');
echo $this->Form->input('Expense.' . $j . '.dollar_amount');
echo $this->Form->input('Expense.' . $j . '.sqft_amount');
$j++;
}
$i++;
}
This code seems to be iterating properly because it spits out the correct input fields. The big problem right now is just getting the fields to pre-populate. It doesn't seem to be in "edit" mode and I'm worried that that will be a problem down the road when I try to save the data.
Also, I have tried it with and without $this->form->create('Category') at the top. It doesn't seem to make a difference.
The $categories array looks like this ...
array(
(int) 0 => array(
'Category' => array(
'category_id' => '1',
'category_name' => 'Category 1',
'category_index' => '1',
'project_id' => '1'
),
'Project' => array(
'project_id' => '1',
'project_name' => '131 Anndale Dr',
'project_sqft' => '1700',
'project_cost' => '318',
'project_cost_per_sqft' => '0'
),
'Expense' => array(
(int) 0 => array(
'expense_id' => '2',
'expense_name' => 'Nails',
'category_id' => '1',
'dollar_amount' => '50',
'sqft_amount' => '1',
'expense_index' => '2'
),
(int) 1 => array(
'expense_id' => '1',
'expense_name' => 'Wood',
'category_id' => '1',
'dollar_amount' => '99',
'sqft_amount' => '1',
'expense_index' => '1'
)
)
),
(int) 1 => array(
'Category' => array(
'category_id' => '3',
'category_name' => 'Category 2',
'category_index' => '2',
'project_id' => '1'
),
'Project' => array(
'project_id' => '1',
'project_name' => '131 Anndale Dr',
'project_sqft' => '1700',
'project_cost' => '318',
'project_cost_per_sqft' => '0'
),
'Expense' => array(
(int) 0 => array(
'expense_id' => '3',
'expense_name' => 'Bed',
'category_id' => '3',
'dollar_amount' => '99',
'sqft_amount' => '2',
'expense_index' => '1'
),
(int) 1 => array(
'expense_id' => '4',
'expense_name' => 'Chair',
'category_id' => '3',
'dollar_amount' => '70',
'sqft_amount' => '1',
'expense_index' => '2'
)
)
)
)
Any help would be greatly appreciated. Thanks!!
Coincidentally, I've been doing almost exactly the same thing today.
In order to automatically populate the form, you need to have your data in $this->data in the view. You can do this by assigning to $this->request->data in your controller.
Then you need to have your field names reflect the structure of that array exactly. So using your example, you might have:
echo $this->Form->input("$i.Category.category_name");
for the category fields, and
echo $this->Form->input("$i.Expense.$j.expense_id");
for your expense fields.
I'm having some trouble trying to paginate related data in a hasMany relationship
I have two models - Collection and Item
Collection hasMany Item
Item belongsTo Collection
In the Collection view.ctp it shows the collection details for the ID being passed along with related data of the Items. It receives this data from a find "first" call.
$this->set('collection', $this->Collection->find('first', $options));
With the resulting array looking like:
array(
'Collection' => array(
'id' => '4fc923f5-0a58-453f-9507-0c0c86106d80',
'name' => '<collection_name>'
),
'Item' => array(
(int) 0 => array(
'id' => '4fc92403-000c-4ed2-9dae-0c0c86106d80',
'name' => 'Item1'
),
(int) 1 => array(
'id' => '4fc9241b-35ec-4810-b511-0c0c86106d80',
'name' => 'Item2'
),
(int) 2 => array(
'id' => '4fc96e5d-ad74-4c05-a9da-0c0c86106d80',
'name' => 'Item3'
),
(int) 3 => array(
'id' => '4fc96e64-a110-4036-bea2-0c0c86106d80',
'name' => 'Item4'
)
)
Now I can get the same results from this paginate call except there is the added 0 index
$this->set('items', $this->paginate('Collection', array('Collection.id'=>$id)));
array(
(int) 0 => array(
'Collection' => array(
'id' => '4fc923f5-0a58-453f-9507-0c0c86106d80',
'name' => '<collection_name>'
),
'Item' => array(
(int) 0 => array(
'id' => '4fc92403-000c-4ed2-9dae-0c0c86106d80',
'name' => 'Item1'
),
(int) 1 => array(
'id' => '4fc9241b-35ec-4810-b511-0c0c86106d80',
'name' => 'Item2'
),
(int) 2 => array(
'id' => '4fc96e5d-ad74-4c05-a9da-0c0c86106d80',
'name' => 'Item3'
),
(int) 3 => array(
'id' => '4fc96e64-a110-4036-bea2-0c0c86106d80',
'name' => 'Item4'
)
)
)
);
I can perform an array_shift on the array returned but then the pagination doesn't work. I can try creating a custom paginate function for this but wanted to know if there's a simplier way to do this with the standard paginate I'm using since it has the data I want, just extra first array parent element [0].
Thanks in advance for any suggestions.
<?php
// Don't pull in child data, we'll fetch that manually.
$this->Collection->contain(); // Assuming you have this behavior.
// Get the collection.
$collection = $this->Collection->find('first', $options);
// Conditions
$this->paginate['Item'] = array(
'conditions' => array(
'Item.collection_id' => $collection['Collection']['id'];
)
);
// Get the items
$items = $this->paginate('Item');
// Return to original child model structure.
array_walk($items, function(&$v) { // Anonymous functions requires PHP 5.3
$v = $v['Item'];
});
// Add it to the collection array.
$collection['Item'] = $items;
?>
That's the way I implement pagination on child models. Give it a shot, might work for you too.
-A
I have associations:
Product -> hasMany -> ProductOption
Product -> hasMany -> ProductImage
I want to find the product data including ProductImage with the ProductOption.id
When I do this:
$this->Basket->Product->ProductOption->find('first',array(
'contain' => array(
'Product' => array(
'ProductOption' => array(
'conditions' => array('ProductOption.id = '.$id)
),
'ProductImage'
)
)
));
I get this:
array(
'ProductOption' => array(
'id' => '46',
'product_id' => '9',
),
'Product' => array(
'id' => '9',
'name' => 'Some product',
)
)
Which is most of what I want but without 'ProductImage' included. How do I get this in the array?
I think you're using too much recursion. If you're using Containable on Product, make the query on it, not ProductOption, like this:
$this->Basket->Product->find('first',array(
'contain' => array(
'ProductOption' => array(
'conditions' => array('ProductOption.id = '.$id)
),
'ProductImage'
)
));
And no need to use Product on 'contain' array, it's already guessed.
(I didn't try this, but almost sure)
I have got two Models in CakePHP 2.2.1:
class Episode extends AppModel {
public $hasAndBelongsToMany = array(
'Tags' => array('className' => 'Tag')
);
}
and
class Tag extends AppModel {
public $hasAndBelongsToMany = array(
'Episode' => array('className' => 'Episode')
);
}
And I want to insert an Episode and a Tag at the same time and link them to each other. From the documentation I got that this would be the right code which I execute inside a controller method:
$this->Tag->create();
$this->Tag->save(array(
'name' => 'Test Tag'
));
$tagid = $this->Tag->getInsertID();
var_dump($tagid); // works
$this->Episode->create();
$this->Episode->save(array(
'Episode' => array(
'title' => 'Test with Tag'
),
'Tags' => array(
'Tag' => array($tagid)
)
));
I get no error message and both records are created, but there is no row inserted into the episodes_tags table. CakePHP does these queries:
Nr Query Err Aff Rws Took (ms)
1 INSERT INTO `pottcast`.`tags` (`name`) VALUES ('Test Tag') 1 1 0
2 INSERT INTO `pottcast`.`episodes` (`title`) VALUES ('Test with Tag') 1 1 0
3 SELECT `EpisodesTag`.`tag_id` FROM `pottcast`.`episodes_tags` AS `EpisodesTag` WHERE `EpisodesTag`.`episode_id` = 9 0 0 0
What did I do wrong?
Try This :
In Episode Model:
public $hasAndBelongsToMany = array(
'Tag' => array(
'className' => 'Tag',
'joinTable' => 'episodes_tags',
'foreignKey' => 'episode_id',
'associationForeignKey' => 'tag_id',
'unique' => 'keepExisting',
));
In your controller:
$this->Episode->save(array(
'Episode' => array(
'title' => 'Test with Tag'
),
'Tag' => array(
'Tag' => $tagid
)
));
Ask if not work for you.