I have an application in CakePHP 4 and am having problems saving associated model data. I have read Saving Associated Data in the Cake documentation but it's very unclear how this applies especially in my use-case.
The application has 3 tables which are relevant to this question:
items
sizes
items_sizes_wanted
The application allows users to request items of clothing (items) and the form to input/save such an item has a dropdown of different sizes (sizes). Each size has a unique ID. A user can select one or more size when saving an item. The items_sizes_wanted table is supposed to hold one (or more) rows depending on the sizes the user selected, with the corresponding item ID. For example if they saved sizes 2, 3 and 4 for Item 999 there would be 3 rows in this table:
size_id | item_id
--------|---------
2 | 999
3 | 999
4 | 999
The code has been baked and the associations in the Table classes look ok:
// src/Model/Table/ItemsSizesWantedTable.php
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('items_sizes_wanted');
$this->belongsTo('Items', [
'foreignKey' => 'item_id',
'joinType' => 'INNER',
]);
$this->belongsTo('Sizes', [
'foreignKey' => 'size_id',
'joinType' => 'INNER',
]);
}
The Entity class for the item also looks ok:
// src/Model/Entity/Item.php
// #property \App\Model\Entity\ItemsSizesWanted[] $items_sizes_wanted
protected $_accessible = [
// ...
'items_sizes_wanted' => true,
// ...
];
In the form where the item gets saved (src/templates/Items/add.php) I have used the Form Helper and named it using dot notation:
<?php
// Note that $sizes is an array of key/value pairs from the
// 'sizes' table.
?>
<?= $this->Form->control('items_sizes_wanted.size_id', ['options' => $sizes, 'multiple' => 'multiple']) ?>
When rendered in the browser this produces a valid array syntax name. The <option>'s rendered inside all have valid ID's, i.e. the ones from the sizes table.
<select name="items_sizes_wanted[size_id]" multiple="multiple">
When I save the data in my Controller (src/Controller/ItemsController.php) using the following:
public function add()
{
$item = $this->Items->newEmptyEntity();
if ($this->request->is('post')) {
$item = $this->Items->patchEntity($item, $this->request->getData());
// Edit: some of the entity properties are manually set at this point, e.g.
$item->item_status = 'Wanted';
if ($this->Items->save($item)) {
$this->Flash->success(__('Your item has been listed.'));
}
}
}
The data is saved correctly to the items table and the flash success message, "Your item has been listed." is displayed in the browser.
But - no data is written to items_sizes_wanted.
I'm unsure why this is. The linked docs don't specifically say how the Form Helper should be used, so I'm assuming my syntax for that form field is correct, but it might not be.
If I debug the entity after pressing Save using debug($item); die; in the Controller it has 'items_sizes_wanted' => [ ] even though I selected multiple size options using the form.
Please can somebody help as I'm lost as to what's going wrong here?
Disclaimer: I do not know CakePHP well, but I think I either know the solution, or can at least point you in the right direction.
The reason you're only getting the one selected size, instead of multiple, is because the generated input fields are named items_sizes_wanted[size_id], however, in order for PHP to parse multiple values into an array, they need to be named items_sizes_wanted[size_id][]. When the request parameter ends with [], then PHP will properly parse all request properties into an array.
For example: Here's var_dump($_POST); of a request containing the POST body of items_sizes_wanted[size_id][]=A&items_sizes_wanted[size_id][]=B&items_sizes_wanted[size_id][]=C
array (size=1)
'items_sizes_wanted' =>
array (size=1)
'size_id' =>
array (size=3)
0 => string 'A' (length=1)
1 => string 'B' (length=1)
2 => string 'C' (length=1)
Compare that to a POST body of items_sizes_wanted[size_id]=A&items_sizes_wanted[size_id]=B&items_sizes_wanted[size_id]=C (notice the empty braces at the end of each have been removed):
array (size=1)
'items_sizes_wanted' =>
array (size=1)
'size_id' => string 'C' (length=1)
This is the part where I'm less familiar with CakePHP. I looked over the code
for CakePHP's FormHelper, and based on the template code, I think you need to change your form code in add.php to be something like this (reformatted for readability):
<?php
// Note that $sizes is an array of key/value pairs from the
// 'sizes' table.
?>
<?=
$this->Form->control(
'items_sizes_wanted.size_id',
[
'options' => $sizes,
'multiple' => 'multiple'
'type' => 'selectMultiple'
]
)
?>
Based on the __call() method in FormHelper, You might also be able to write it like this:
$this->Form->selectMultiple(
'items_sizes_wanted.size_id',
[
'options' => $sizes,
'multiple' => 'multiple'
]
);
However, I'm not familiar with the nuances between creating a control($fieldName, $options) and inputType($fieldName, $options), so they might produce different outputs.
I'm currently working in a project in CakePHP 4.x. My project also have many to many associations and it saves ok in the tables, but CakePHP baked it quite differently from yours. Let me show you the differences, maybe it is of some help.
I'll "translate" the names of my entities, tables, etc., to the ones used in your question, ok?
First, a brief: in my project, cake didn't bake models (entity and table) for the relational table. The relational table don't have its own models, and is only refered to in the initialize method from the ItemsTable and WantedSizesTable. There are also minor changes in the Item and WantedSize entities and in the view.
Second, your entity names doesn't comply with Cake's naming conventions, which can lead to many issues. This can even be the cause to the problems you're enduring now. I have changed some names to comply with them, but I'd suggest to you to read it thoroughly: https://book.cakephp.org/4/en/intro/conventions.html.
Third and more important, lets start.
My many-to-many relational mySQL tables doesn't have their own Table models. My SQL does indeed have a items_wanted_sizes table, but the CakePHP project does NOT have corresponding models called ItemsWantedSizesTable nor ItemsWantedSizes. It does have ItemsTable and WantedSizesTable tables and Item and WantedSize entities, and it's all.
Let's see the Table Models. The relational mySQL table items_wanted_sizes is refered only in the tables initialize method of both table models in PHP, like this:
// ItemsTable.php
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('items');
$this->setDisplayField('item_name');
$this->setPrimaryKey('id');
// ...
// Other associations...
// ...
// The relational mysql table only shows here:
$this->belongsToMany('WantedSizes', [
'foreignKey' => 'item_id', // Item Id field from the relational table
'targetForeignKey' => 'wanted_size_id', // Size Id field from the relational table
'joinTable' => 'items_wanted_sizes',
]);
}
The same happens on WantedSizesTable:
// WantedSizesTable.php
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('wanted_sizes');
$this->setDisplayField('wanted_size_name');
$this->setPrimaryKey('id');
// ...
// Other associations...
// ...
// The relational mysql table only shows here:
$this->belongsToMany('Items', [
'foreignKey' => 'wanted_size_id', // Size Id fieldname from the relational table
'targetForeignKey' => 'item_id', // Item Id fieldname from the relational table
'joinTable' => 'items_wanted_sizes',
]);
}
Regarding to the entities models, I also don't have a relational entity model. Both Item and WantedSize entity models refer to each other, but, contrary to your case, they don't refer to the relational table (only to each other):
// src/Model/Entity/Item.php
// #property \App\Model\Entity\WantedSize[] $wanted_sizes // NOT item_wanted_sizes
protected $_accessible = [
// ...
'wanted_sizes' => true, // NOT item_wanted_sizes
// ...
];
Same in WantedSize:
// src/Model/Entity/WantedSize.php
// #property \App\Model\Entity\Item[] $items // NOT item_wanted_sizes
protected $_accessible = [
// ...
'items' => true, // NOT item_wanted_sizes
// ...
];
Now we saw our models, lets jump the add (or edit) action view. With the associations correctly set, I only needed to do this:
// src/templates/Items/add.php
echo $this->Form->control('wanted_sizes._ids', ['options' => $wantedSizes]);
I didn't even needed to tell FormHelper it's a multiselect, because it is in the table configurations.
The HTML generated is quite different from yours (like 404 also answered above):
<select name="wanted_sizes[_ids][]" multiple="multiple" id="wanted-sizes-ids">
<option value="1">Some wanted size...</option>
<!-- ... -->
</select>
This worked perfectly fine for me, saving data in the relational table in mysql.
In Cakephp4 one thing to check. If the entities are not showing the associated data after being patched prior to saving. You can test by dumping the entity after its patched in the controller.The associated data should show there.
$discount = $this->Discounts->patchEntity($discount, $this->request->getData());
dd($discount);
Check the Entity. Is the associated data in the $_accessible array? The fields that you update need to be in this array but also the associated models/tables.
class Discount extends Entity
{
protected $_accessible = [
...
'products' => true,
...
];
}
https://api.cakephp.org/4.0/class-Cake.ORM.Entity.html#$_accessible
Related
I am new to laravel.
I have an issue when I am trying to update or create record in DB.
I have a table called DspAccountFee with this columns:
I want to create record of dsp_account_id + screen_type when the combination not exists, and to update if the combination exists.
this is my code: (just tried to update the first row keys of -> dsp_account_id(5187) + screen type (ctv). However nothing changed.
DspAccountFee::updateOrCreate(
['dsp_account_id' => $dsp_account_id, 'screen_type' => 'ctv'],
['pmp_percent' =>$fields['fee_ctv_pmp_percent'], 'omp_percent' => $fields['fee_ctv_omp_percent']]
);
When I print the values before the DB operation they exists:
\Log::info("dsp_account:");
\Log::info($dsp_account_id);
\Log::info("ctv pmp percent:");
\Log::info($fields['fee_ctv_pmp_percent']);
\Log::info("ctv omp percent:");
\Log::info($fields['fee_ctv_omp_percent']);
\Log::info("app pmp percent:");
What I am missing why it is not update the db? Nothing in logs and No exception
this is my method in the model
protected $fillable = array(
'dsp_account_id', 'screen_type'
);
Check the corresponding model and make sure that those columns exist in the
$fillable property. It should look somewhat like this.
protected $fillable = [
'dsp_account_id',
'screen_type',
'pmp_percent',
'omp_percent'
];
Your updateOrCreate syntax looks okay.
To update the updated_at column in your database, you can use the touch() method: you'll need to edit your code to something like this
$foo = DspAccountFee::updateOrCreate([
'dsp_account_id' => $dsp_account_id,
'screen_type' => 'ctv'
],
[
'pmp_percent' => $fields['fee_ctv_pmp_percent'],
'omp_percent' => $fields['fee_ctv_omp_percent']
]);
$foo->touch();
I am currently using CakePHP to serve a crud based api for some ticketing logic I wrote. I am running into an issue where I am attempting to change a belongsTo association and data within the new association and it is not persisting.
The controller doing the persisting looks like this:
<?php
class TasksController extends Cake\Controller\Controller
{
public function initialize()
{
parent::initialize();
}
public function edit(array $ids): void
{
$validationErrors = [];
$tasks = $this->Tasks->find('all')
->contain($this->setAssociations($query))
->where([$this->Model->getAlias().'.id IN' => $ids]);
foreach ($tasks as $id => $task) {
$this->Tasks->patchEntity($task, $this->request->getQuery()[$id], [
'associated' => ['Asset']
]);
}
if ($this->Tasks->saveMany($tasks)) {
$this->response = $this->response->withStatus(200);
} else {
// Handle errors
}
// Render json output for success / errors
$this->set($this->createViewVars(['entities' => $tasks], $validationErrors));
}
}
The association for an asset in the task table looks like this:
<?php
class TasksTable extends Cake\ORM\Table
{
public function initialize(array $config)
{
$this->belongsTo('Asset', [
'className' => 'Assets',
'foreignKey' => 'asset_id',
'joinType' => 'LEFT'
]);
}
}
These build rules are attached to the asset table:
<?php
class AssetsTable extends Cake\ORM\Table
{
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->isUnique(['number', 'group_id'], 'The Number you selected is in use'));
$rules->add($rules->isUnique(['name', 'group_id'], 'The Name you selected is in use'));
}
}
The body of the request I am sending looks like this:
{
"421933": {
"description": "This task was edited by the api!",
"asset": {
"id": "138499",
"description": "This asset was edited by they api!",
"name": "105",
"number": "6"
}
}
}
Basically the name 105 and number 6 are being flagged as not being unique, because they are already set to those values on asset 138499. The query is is instead trying to edit name 105 and number 6 into the Asset entity that is presently associated with the Task entity (645163), which is triggering the isUnquie build rules to fail.
You can see this by printing the $tasks before the saveMany call in the above controller:
Array
(
[0] => App\Model\Entity\Task Object
(
[id] => 421933
[description] => This task was edited by the api!
.....
[asset] => App\Model\Entity\Asset Object
(
[id] => 645163
[name] => 105
[description] => This asset was edited by they api!
[number] => 6
....
)
)
)
It seems like this editing Asset 138499 as an association of Task 421933 should work as it is appears to be possible to save belongsTo associations in this fashion in the CakePHP docs, as documented here:
<?php
$data = [
'title' => 'First Post',
'user' => [
'id' => 1,
'username' => 'mark'
]
];
$articles = TableRegistry::getTableLocator()->get('Articles');
$article = $articles->newEntity($data, [
'associated' => ['Users']
]);
$articles->save($article);
Is it possible to associate a belongsTo association and edit it in the same transaction? If so how should my request or code be structured differently?
Thanks!
As hinted in the comments, you're running into a limitation of the marshaller there, for which there is no overly startightforward workaround yet (you might want to open an issue over at GitHub, maybe someone want's to take up the task of adding support for this type of marshalling/saving).
The example in the book isn't really the best to go by here, in fact I'd personally remove/change it, it would only work when mass assigning the primary key is allowed (which by default for baked entities it isn't), and it would also only work when creating new records, ie when the entity that is to be saved is marked as "new". However doing this would introduce all sorts of quirks, like validation rules would see the data as "create" (new), application rules would see the entity as "update" (not new), but not receive any of the existing/original data, same for other saving stage callbacks, which would mess up stuff like auditing or the like, basically anything where you need the new data and the original data.
Long story short, even if you could get it somewhat working like with that example, you'd just introduce other problems further down the road, so better forget about that for now. The only workaround-ish thing that comes to my mind right now, would be comparing the given asset primary key, loading the existing asset entity manually, and replacing the already loaded one before patching in your data, something along the lines of this:
foreach ($tasks as $task) {
$data = $this->request->getData($task->id);
$assetId = (int)\Cake\Utility\Hash::get($data, 'asset.id');
if ($task->asset->id !== $assetId) {
$task->asset = $this->Tasks->Asset->get($assetId);
}
$this->Tasks->patchEntity($task, $data, [
'associated' => ['Asset']
]);
}
Note that I've changed your getQuery() usage to getData(), as it seemed unlikely that you're passing that data via the query string.
So I am trying to generate a dynamic select for Product Table form
instead of writing all the options like this: (code from productType)
->add('id_cat', ChoiceType::class, [
'choices' => [
'cosmetique' =>'1',
'vetement' => '2',
'parfums' => '3',
],
])
I want the choices to be generated from another table Category(id,cat_name) .
For example instead of 'cosmetique' it shows cat_name and instead of 1 it shows id
(and those are values generated from the database from table Category)
I wrote this function that return all the values from table Category in a table $tab
public function cat (CategoryRepository $categoryRepository)
{
$allcat=$categoryRepository->findAll();
$tab=[];
foreach($allcat as $cat)
{
$tab=$cat->getId();
$tab[$tab]=$cat->getCatname();
}
return $tab;
}
I dont know where exactly I should place it? and how to send that $tab to ProductType.php page
If you have relation between the two entities (and i guess you have) you can easily use the entity type from Symfony to avoid problem like this which will show all the content inside the category table and submit the chosen object to be saved depending on your association :
$builder->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'cat_name',
]);
Or if you want to keep your way you can pass the array as an option to your form but its not a good practice.
Why not just make tab[] something like having a field named 'choices' statically built into it before populating and then add every I'd and category to it and return the table and return results of array wherever?
tab['choices']=$cat->getID();
tab['choices'][tab['choices']]=$cat->getCatname();
And then
for each ($choices as $choice)
For each ($choice as $category)
Print_r( $category)
How to populate a select dropdown in cakephp3 from a database table.
Currently Cakephp produces an array (list of options) which is numeric indexed.
The option values should have id of database records (should not be the numeric keys generated by Cakephp)
Departments(table) is a taxonomy to classify events.
Events(table) which should store department based list of events.
A event may belongs to many departments. How do I achieve this?
EventsController.php
<?php $department_results = $connection->execute('SELECT departmentname FROM department')->fetchAll('assoc'); // list of departments
$this->set('departmentvalues', $department_results );
Events: add.ctp
<?php echo $this->Form->input('department', array('label' => false,
'div' => false,
'type' => 'select',
'multiple'=>'checkbox',
'legend' => 'false',
'options' => $departmentvalues,
'empty' => 'Please select a Department'
));
Objective:
A select dropdown with values from database, option values should be id of the database record
Expected result:
<select name="department"><option value="2">Maths</option><option value="4">Physics</option></select>
Issue:
cakephp generates numeric indexed array of options.
<select name="department"><option value="0">Maths</option><option value="1">Physics</option></select>
You should really use the CakePHP standard for querying your models, instead of raw SQL, especially in the case of a simple list.
Something like the below should do what you need:
$department_results = $this->Departments->find('list')
->hydrate(false)
->fields(['id','departmentname'])
->toArray();
$this->set('departmentvalues', $department_results);
Note that you will need to include the fields as you have named your column departmentname. By default, a find('list') should return id and name fields.
Another option is to set the displayField this way :
`class DepartmentsTable extends Table {
public function initialize(array $config) {
parent::initialize($config);
$this->displayField('departmentname');
}
}`
In the controller you may just call the method find('list') this way :
$departments= TableRegistry::get("Departments");
$departmentvalues=$departments->find('list')
->hydrate(false)
->toArray();
$this->set('departmentvalues', $department_results);
I'm having an annoying problem. I'm trying to find out what fields of a form were changed, and then insert that into a table. I managed to var_dump in doUpdateObjectas shown in the following
public function doUpdateObject($values)
{
parent::doUpdateObject($values);
var_dump($this->getObject()->getModified(false));
var_dump($this->getObject()->getModified(true));
}
And it seems like $this->getObject()->getModified seems to work in giving me both before and after values by setting it to either true or false.
The problem that I'm facing right now is that, some how, sfWidgetFormSelect seems to be saving one of my fields as a string. before saving, that exact same field was an int. (I got this idea by var_dump both before and after).
Here is what the results on both var dumps showed:
array(1) {["annoying_field"]=> int(3)} array(1) {["annoying_field"]=>string(1)"3"}
This seems to cause doctrine to think that this is a modification and thus gives a false positive.
In my base form, I have
under $this->getWidgets()
'annoying_field' => new sfWidgetFormInputText(),
under $this->setValidators
'annoying_field' => new sfValidatorInteger(array('required' => false)),
and lastly in my configured Form.class.php I have reconfigured the file as such:
$this->widgetSchema['annoying_field'] = new sfWidgetFormSelect(array('choices' => $statuses));
statuses is an array containing values like {""a", "b", "c", "d"}
and I just want the index of the status to be stored in the database.
And also how can I insert the changes into another database table? let's say my Log table?
Any ideas and advice as to why this is happen is appreciated, I've been trying to figure it out and browsing google for various keywords with no avail.
Thanks!
Edit:
ok so I created another field, integer in my schema just for testing.
I created an entry, saved it, and edited it.
this time the same thing happened!
first if you what the status_id to be saved in the database, you should define your status array like this:
{1 => "a", 2 => "b", 3 => "c", 4 => "d"}
So that way he know that 1 should be rendered like "a" and so on. Then, when saving, only the index should be saved.
About saving in another database, my advise is to modify the doSave method defined by the Form class yo match your needs. I only know how Propel deals with it, maybe this could help:
the doSave method dose something like this:
protected function doSave($con = null)
{
if (null === $con)
{
$con = $this->getConnection();
}
$old = $this->getObject()->getModifiedValues($this);//Define this
$new_object = new Log($old);//Create a new log entry
$new_object->save($con));//save it!
$this->updateObject();
$this->getObject()->save($con);
// embedded forms
$this->saveEmbeddedForms($con);
}
Hope this helps!
Edit:
This is an example extracted from a model in one of my applications and its working ok:
Schema:
[...]
funding_source_id:
type: integer
required: true
[...]
Form:
$this->setWidget('funding_source_id', new sfWidgetFormChoice(array(
'choices' => array(1 => 'asdads', 2 => '123123123' , 3 => 'asd23qsdf'),
)));
$this->setValidator('funding_source_id', new sfValidatorChoice(array(
'choices' => array(1 => 'asdads', 2 => '123123123' , 3 => 'asd23qsdf'),
'required' => true
)));
About the log thing, that could be quite more complex, you should read the current implementation of the doSave method in the base form class, currently sfFomrObject on Symfony1.4., and when and how it delegates object dealing with modified values.
Okay,
It turns out I forgot to do a custom validator to use the array key instead.