I'm having trouble updating existing data in my join table. Here it goes...
I have 2 models SalesOrders and Products that have a belongsToMany association through LineItems.
products
+-------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------------------+------+-----+---------+----------------+
| id | int(11) unsigned | NO | PRI | NULL | auto_increment |
| name | int(11) | NO | | NULL | |
+-------------+---------------------+------+-----+---------+----------------+
sales_orders
+--------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+---------------------+------+-----+---------+----------------+
| id | int(11) unsigned | NO | PRI | NULL | auto_increment |
| order_number | int(11) | NO | | NULL | |
+--------------+---------------------+------+-----+---------+----------------+
line_items
+----------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------------+------+-----+---------+----------------+
| id | int(11) unsigned | NO | PRI | NULL | auto_increment |
| sales_order_id | int(11) unsigned | NO | | NULL | |
| product_id | int(11) unsigned | NO | | NULL | |
| qty | int(11) | NO | | NULL | |
+----------------+---------------------+------+-----+---------+----------------+
Creating a new sales order works as expected. I can add multiples products and quantities to a sales order on the add new sales order page. All the correct data is added to the join table.
The problem I'm having is when I try to edit an existing sales order. For example, I want to change the quantity of a certain line item from 5 to 2. I open the edit page for the sales order I wish to modify, change the quantity on the desired line item, and submit the form. The form submits successfully but the join table is not updated. This seems like pretty basic functionality that I can't get working. All of my code was baked with a few small modifications.
SalesOrdersTable:
$this->belongsToMany('Products', [
'foreignKey' => 'sales_order_id',
'targetForeignKey' => 'product_id',
'through' => 'LineItems',
]);
ProductsTable:
$this->belongsToMany('SalesOrders', [
'foreignKey' => 'product_id',
'targetForeignKey' => 'sales_order_id',
'through' => 'LineItems',
]);
LineItemsTable:
$this->belongsTo('Products', [
'foreignKey' => 'product_id',
'joinType' => 'INNER'
]);
$this->belongsTo('SalesOrders', [
'foreignKey' => 'sales_order_id',
'joinType' => 'INNER'
]);
SalesOrdersController edit:
public function edit($id = null)
{
$salesOrder = $this->SalesOrders->get($id, [
'contain' => ['Products']
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$salesOrder = $this->SalesOrders->patchEntity($salesOrder, $this->request->data);
if ($this->SalesOrders->save($salesOrder)) {
$this->Flash->success(__('The sales order has been saved.'));
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error(__('The sales order could not be saved. Please, try again.'));
}
}
$products = $this->SalesOrders->Products->find('list', ['limit' => 200]);
$this->set(compact('salesOrder', 'products'));
$this->set('_serialize', ['salesOrder']);
}
SalesOrders template edit.ctp
echo $this->Form->input('order_number');
echo $this->Form->input('products.0.id', [
'type' => 'select',
'options' => $products
]);
echo $this->Form->input('products.0._joinData.qty');
The SalesOrder entity looks like this when submitting the form to change the quantity from 5 to 2.
object(App\Model\Entity\SalesOrder) {
'id' => (int) 1,
'order_number' => 'SO1111',
'products' => [
(int) 0 => object(App\Model\Entity\Product) {
'id' => '1',
'name' => 'Acme Widget 1',
'_joinData' => object(App\Model\Entity\LineItem) {
'id' => (int) 1,
'product_id' => (int) 1,
'sales_order_id' => (int) 1,
'qty' => (int) 2,
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'qty' => true
],
'[original]' => [
'qty' => (int) 5
],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'LineItems'
},
'[new]' => false,
'[accessible]' => [
'*' => true,
'_joinData' => true
],
'[dirty]' => [
'_joinData' => true
],
'[original]' => [
'_joinData' => object(App\Model\Entity\LineItem) {
'id' => (int) 1,
'product_id' => (int) 1,
'sales_order_id' => (int) 1,
'qty' => (int) 2,
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'qty' => true
],
'[original]' => [
'qty' => (int) 5
],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'LineItems'
}
],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Products'
}
],
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'SalesOrders'
}
As you can see the dirty property on the SalesOrder is empty. It's not picking up that the Products _joinData has been modified.
If I add $salesOrders->dirty('products', true); just before save() is called then join table gets updated. While this works, and I could write some logic to handle the update this way, I feel there is better/proper way to do this.
Any help is appreciated, thanks in advance.
Turns out this was a cake bug in 3.3 that wasn't resolved until the 3.3.2 release. Once I updated everything worked as expected. github.com/cakephp/cakephp/pull/9276
Related
I'm attempting to ->find('all') & contain the hasMany() Schedules which belongsTo() Services within the Services controller index method. A Service can actually have zero to many Schedules. Testing and experience from the Services view method has lead me believe that the issue is due to a DB field that is using a keyword. It's a field that I cannot see a need for at this time. Unfortunately, I don't have the ability to change database structure.
I attempted ->contain() and specified the fields I wanted, including the primary fields of the Schedules contain. The results were the same as Attempt #1 listed below. I have successfully used that in another model.
I attempted ->selectAllExcept($tableObj, ['FieldToExclude'])->contain(). I also attempted a beforeFind() within the SchedulesTable.php after research here but the function never seemed to be called.
I'm able to circumvent the issue in the view method by specifying the fields in a find directly to the Services table. However, this is not a viable option within the index method. I can do a loop to get the related records, but that's timely and defeats the fantastic benefits of Cake. Contain is awesome and is exactly what I need here.
Are the belongsTo & hasMany set up incorrectly? Am I missing something in the coding of $query? I've read through the documentation and I know that I am missing what is probably obvious to so many. I know I am getting to the point of frustration that won't let me think clearly. ;) I've even stepped away for a day hoping that would help.
Thank you in advance for any help that can be provided. It will be greatly appreciated.
ServicesTable.php
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('OSCL');
$this->setDisplayField('DocNum');
$this->setPrimaryKey('DocNum');
$this->belongsTo('Locations', [
'className' => 'Locations',
'bindingKey' => 'Address',
'foreignKey' => 'BPShipCode',
'joinType' => 'INNER'
]
);
$this->hasMany('Schedules', [
'className' => 'Schedules',
'foreignKey' => 'SrcvCallID',
'bindingKey' => 'CallID'
]
);
}
And SchedulesTable.php
use Cake\Event\Event;
use ArrayObject;
class SchedulesTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('SCL6');
$this->setDisplayField('Technician');
$this->setPrimaryKey(['SrcvCallID', 'Line']);
$this->belongsTo('Services', [
'className' => 'Services',
'foreignKey' => 'SrcvCallID',
'bindingKey' => 'CallID'
]
);
$this->hasOne('Employees', [
'className' => 'Employees',
'foreignKey' => 'empID',
'bindingKey' => 'Technician'
]
);
}
/*
Added to class after the original 3 attempts.
Re-attempted the queries. Function made no difference.
Debug & die within the function showed that the function was never called.
*/
public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary)
{
return $query->selectAllExcept($this, ['Close']);
}
}
Attempt #1 returns the expected # of records but the schedules array is empty. I don't get any error messages and the query log doesn't show any attempts to access the Schedules table.
$query = $this->Services->find('all')
->where($conditions)
->order([$sort => $direction])
->contain(['Schedules']);
Debug of $query->toArray(); The table has over 100 fields. I've removed many here for visual ease.
\src\Controller\ServicesController.php (line 124)
[
(int) 0 => object(App\Model\Entity\Service) {
'callID' => (int) 361893,
'subject' => 'Printer jam',
'customer' => 'C202044',
'custmrName' => 'Some Company',
'contctCode' => (int) 986,
'manufSN' => '',
'internalSN' => '16J151800861',
'createDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'createTime' => (int) 1612,
'closeDate' => null,
'closeTime' => null,
'DocNum' => (int) 316939,
'Series' => (int) 30,
'schedules' => [],
'[new]' => false,
'[accessible]' => [],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Services'
},
]
Query Log
SELECT
Services.callID AS [Services__callID], Services.subject AS [Services__subject], Services.customer AS [Services__customer], Services.custmrName AS [Services__custmrName], ...
FROM
OSCL Services
WHERE
(
Services.status = 7
OR Services.status = -3
)
ORDER BY Services.DocNum DESC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY
SELECT
(
COUNT(*)
) AS [count]
FROM
OSCL Services
WHERE
(
Services.status = 7
OR Services.status = -3
)
Attempt #2 generates a SQL query error "Error: SQLSTATE[42000]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Incorrect syntax near the keyword 'Close'". Makes sense because the default behavior for a model is to select all of the fields.
$query = $this->Services->find('all')
->where($conditions)
->order([$sort => $direction])
->select($this->Services->Schedules);
Attempt #3. I tried to pass the table object to ->selectAllExcept() and exclude the problem field. The query builder builds the query incorrectly by using the fields from the Schedules table as if they were part of the Services table
$query = $this->Services->find('all')
->where($conditions)
->order([$sort => $direction])
->selectAllExcept($this->Services->Schedules, ['Close'])
->contain(['Schedules']);
Query built by the query builder. SrcvCallID, Line & Technician are all fields of the Schedules table, not Services.
SELECT
Services.SrcvCallID AS [Services__SrcvCallID], Services.Line AS [Services__Line], Services.Technician AS [Services__Technician], ...
FROM
OSCL Services
WHERE
(Services.status = :c0 OR Services.status = :c1)
ORDER BY
Services.DocNum DESC
OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY
Here is an example of the expected results manually created from the view function.
[
(int) 0 => object(App\Model\Entity\Service) {
'callID' => (int) 361893,
'subject' => 'Printer jam',
'customer' => 'C202044',
'custmrName' => 'Some Company',
'contctCode' => (int) 986,
'manufSN' => '',
'internalSN' => '16J151800861',
'createDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'createTime' => (int) 1612,
'closeDate' => null,
'closeTime' => null,
'DocNum' => (int) 316939,
'Series' => (int) 30,
'schedules' => [
(int) 0 => object(App\Model\Entity\Schedule) {
'SrcvCallID' => (int) 361893,
'Line' => (int) 1,
'Technician' => (int) 243,
'StartDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-18 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'StartTime' => (int) 800,
'EndDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-18 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'EndTime' => (int) 900,
'U_NB_ChkInDate' => null,
'U_NB_ChkInTime' => null,
'U_NB_ChkOutDate' => null,
'U_NB_ChkOutTime' => null,
'SignData' => null,
'Duration' => (float) 1,
'DurType' => 'H',
'Sched_Closed' => 'N',
'U_NB_TechRate' => (float) 50,
'U_NB_FollowUp' => null,
'[new]' => false,
'[accessible]' => [],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Schedules'
},
(int) 1 => object(App\Model\Entity\Schedule) {
'SrcvCallID' => (int) 361893,
'Line' => (int) 2,
'Technician' => (int) 243,
'StartDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'StartTime' => (int) 1600,
'EndDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'EndTime' => (int) 1630,
'U_NB_ChkInDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'U_NB_ChkInTime' => (int) 1600,
'U_NB_ChkOutDate' => object(Cake\I18n\FrozenTime) {
'time' => '2020-03-17 00:00:00.000000-04:00',
'timezone' => 'America/New_York',
'fixedNowTime' => false
},
'U_NB_ChkOutTime' => (int) 1630,
'SignData' => null,
'Duration' => (float) 30,
'DurType' => 'M',
'Sched_Closed' => 'N',
'U_NB_TechRate' => (float) 150,
'U_NB_FollowUp' => 'Y',
'[new]' => false,
'[accessible]' => [],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Schedules'
}
],
'[new]' => false,
'[accessible]' => [],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Services'
},
]
Again, thanks for any assistance.
I have this category and subcategory table.
The field that has the value NULL for parent_id is a category, and the ones that use parent_id are subcategories.
My tree view has subcategories assigned only to categories, so no subcategories for subcategories.
+----+---------------------+-----------+
| id | name | parent_id |
+----+---------------------+-----------+
| 1 | Laptop | NULL |
| 2 | TV | NULL |
| 3 | Tablet & Mobile | NULL |
| 4 | PC | NULL |
| 5 | Laptops | 1 |
| 6 | Laptop accessories | 1 |
| 7 | TV accessories | 2 |
If I do a SELECT * and spice it with a while I get the following array.
'category' => [
{
'name' => 'Laptop',
'id' => '1',
'parent_id' => undef
},
{
'name' => 'TV',
'id' => '2',
'parent_id' => undef
},
{
'name' => 'Table & mobile',
'id' => '3',
'parent_id' => undef
},
{
'name' => 'PC',
'id' => '4',
'parent_id' => undef
},
{
'name' => 'Laptops',
'id' => '5',
'parent_id' => '1'
},
{
'name' => 'Laptop accessories ',
'id' => '6',
'parent_id' => '1'
},
{
'name' => 'TV accessories',
'id' => '7',
'parent_id' => '2'
}
The structure I'm looking for should look like this.
category 1
-- subcategory 1.1
-- subcategory 1.2
category 2
-- subcategory 2.1
What I get now looks like
category 1
subcategory 1.1
category 2
subcategory 1.2
subcategory 2.1
#!/usr/bin/perl
use strict;
use warnings;
my %struct = (category => [
{
'name' => 'Laptop',
'id' => '1',
'parent_id' => undef
},
{
'name' => 'TV',
'id' => '2',
'parent_id' => undef
},
{
'name' => 'Table & mobile',
'id' => '3',
'parent_id' => undef
},
{
'name' => 'PC',
'id' => '4',
'parent_id' => undef
},
{
'name' => 'Laptops',
'id' => '5',
'parent_id' => '1'
},
{
'name' => 'Laptop accessories ',
'id' => '6',
'parent_id' => '1'
},
{
'name' => 'TV accessories',
'id' => '7',
'parent_id' => '2'
}
]);
my %tree;
for my $cat (grep ! defined $_->{parent_id}, #{ $struct{category} }) {
$tree{ $cat->{id} } = $cat;
}
for my $cat (grep defined $_->{parent_id}, #{ $struct{category} }) {
$tree{ $cat->{parent_id} }{subcat}{ $cat->{id} } = $cat;
}
use Data::Dumper; print Dumper \%tree;
I try to realize a bolongsToMany association with additional data in the join table. The join table has columns for the foreign keys and an additional column 'context'
articlesController:
$article = $this->Articles->patchEntity($article, $this->request->getData());
debug($article);die;
and in a Plugin-Behavior:
public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options)
{
$data['Categories.Categories'] = ['id' => '1', '_joinData' => ['context' => 'Tag']];
debug($data);
}
I expect the context beeing saved in the join table, but it doesn't. The debgger says:
/plugins/Categories/src/Model/Behavior/CategorizeableBehavior.php (line 37)
object(ArrayObject) {
name => 'dfa'
description => 'er'
Categories.Categories => [
'id' => '1',
'_joinData' => [
'context' => 'Tag'
]
]
}
/src/Controller/ArticlesController.php (line 56)
object(App\Model\Entity\Article) {
'name' => 'dfa',
'description' => 'er',
'[new]' => true,
'[accessible]' => [
'name' => true,
'description' => true,
'created' => true,
'modified' => true
],
'[dirty]' => [
'name' => true,
'description' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Articles'
}
Where's my category and context. What's wrong with my code?
If you want to attach join data it needs to be in a nested array.
$data['Categories.Categories'] = [
['id' => '1', '_joinData' => ['context' => 'Tag']]
];
It has to be a nested array so that you could attached multiple records (if you wanted too).
On cakePHP 3.4, i have 3 tables with a belongs to and has many relationship: Ingredients, Products and IngredientsProducts:
class IngredientsTable extends Table
{
public function initialize(array $config)
{
// Use through option because it looks like you
// have additional data on your IngredientsProducts table
$this->belongsToMany('Products', [
'through' => 'IngredientsProducts',
]);
}
}
class ProductsTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Ingredients', [
'through' => 'IngredientsProducts',
]);
}
}
class IngredientsProductsTable extends Table
{
public function initialize(array $config)
{
$this->belongsTo('Ingredients');
$this->belongsTo('Products');
}
}
What I want to accomplish is that when I insert a new product, I would like to insert also the ingredient_id(s) and field qty of each ingredient that is related to that product, on the IngredientsProducts joiner table.
I've been reading the cookbook, and saw that when saving aditional data on the joiner table (in my case the field qty as stated below), you have to use the _joinData property, so my add view looks like this:
<?php
$this->Form->create($product);
// fields of the Products table
echo $this->Form->control('name',array('class' => 'form-control'));
echo $this->Form->control('retail_price');
echo $this->Form->control('best_before');
echo $this->Form->control('comments');
// ingredient_id and qty fields of the ingredients_products table
echo $this->Form->control('ingredients.0._joinData.ingredient_id',['options' => $ingredients);
echo $this->Form->control('ingredients.0._joinData.qty');
//and repetition of these last two as ingredients.1._joinData to ingredients.N._joinData
$this->Form->button(__('Save'));
$this->Form->end();
?>
and on the controller the add method looks like this:
$product = $this->Products->newEntity();
if ($this->request->is('post')) {
$product = $this->Products->patchEntity($product, $this->request->getData(),[
'associated' => ['Ingredients._joinData']
]);
if ($this->Products->save($product)) {
$this->Flash->success(__('The product has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The product could not be saved. Please, try again.'));
}
$this->set(compact('product'));
$this->set('_serialize', ['product']);
However when I submit it doesn't save anything. The debug displays the following being posted:
object(App\Model\Entity\Product) {
'name' => 'salada',
'retail_price' => (float) 23,
'best_before' => (int) 234,
'comments' => 'wer',
'directions' => 'werwewerer',
'ingredients' => [
(int) 0 => object(App\Model\Entity\Ingredient) {
'_joinData' => object(Cake\ORM\Entity) {
'ingredient_id' => (int) 4,
'qty' => (float) 100,
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'ingredient_id' => true,
'qty' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'IngredientsProducts'
},
'[new]' => true,
'[accessible]' => [
'*' => true,
'id' => false
],
'[dirty]' => [
'_joinData' => true
],
'[original]' => [
'_joinData' => [
'ingredient_id' => '4',
'qty' => '100'
]
],
'[virtual]' => [],
'[errors]' => [
'name' => [
'_required' => 'This field is required'
]
],
'[invalid]' => [],
'[repository]' => 'Ingredients'
},
(int) 1 => object(App\Model\Entity\Ingredient) {
'_joinData' => object(Cake\ORM\Entity) {
'ingredient_id' => (int) 5,
'qty' => (float) 200,
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'ingredient_id' => true,
'qty' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'IngredientsProducts'
},
'[new]' => true,
'[accessible]' => [
'*' => true,
'id' => false
],
'[dirty]' => [
'_joinData' => true
],
'[original]' => [
'_joinData' => [
'ingredient_id' => '5',
'qty' => '200'
]
],
'[virtual]' => [],
'[errors]' => [
'name' => [
'_required' => 'This field is required'
]
],
'[invalid]' => [],
'[repository]' => 'Ingredients'
}
],
'[new]' => true,
'[accessible]' => [
'*' => true,
'id' => false
],
'[dirty]' => [
'name' => true,
'retail_price' => true,
'best_before' => true,
'comments' => true,
'directions' => true,
'ingredients' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Products'
}
Does anybody happen to know how to make this work?
just in case here are the table structures on mySQL:
TABLE `Ingredients` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`category_id` int(11) NOT NULL,
`measure_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Table products
TABLE `Products` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`retail_price` float NOT NULL,
`best_before` int(11) NOT NULL,
`comments` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
TABLE `ingredients_products` (
`ingredient_id` int(11) NOT NULL,
`product_id` int(11) NOT NULL,
`qty` double NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Any help or directions would be kindly appreciated!
Thanks
When saving fails, check the errors info
Whenever saving an entity doesn't work, check the errors info:
'[errors]' => [
'name' => [
'_required' => 'This field is required'
]
],
Your validation rules define the Ingredients.name field as required, but it's not present in your form, hence saving fails.
Supply the primary key on the target association
Take a closer look at the examples in the docs for saving join data, the data structure looks a little different to yours.
From your usage of ingredient_id I suspect that you want to link the product to existing ingredients, placing ingredient_id in the _joinData however is not how this works. Editing/linking existing records requires the primary key of the record to be present in either the special _ids key, or on the primary key field of the target association (Ingredients).
So in your case where you want to store additional join data, you have to use the id property for the ingredient, that way the marshaller knows that it needs to load an existing record:
echo $this->Form->control('ingredients.0.id', [
// defining the type is required, as the input type
// guessing only recognizes `_ids` fields, or fields
// with `_id` appended as possible select types
'type' => 'select'
'options' => $ingredients
]);
echo $this->Form->control('ingredients.0._joinData.qty');
See also
Cookbook > Database Access & ORM > Saving Data > Converting BelongsToMany Data
Cookbook > Database Access & ORM > Saving Data > Saving BelongsToMany Associations
Cookbook > Database Access & ORM > Saving Data > Saving Additional Data to the Join Table
I'm having trouble saving and updating hasMany associations. It seems that Cake can't patch the entity correctly somehow.
ProductGroups Table:
$this->hasMany('ProductIdentities', [
'foreignKey' => 'product_group_id',
'saveStrategy' => 'replace'
]);
ProductIdentities Table:
$this->belongsTo('ProductGroups', [
'foreignKey' => 'product_group_id'
]);
ProductGroupsController:
// patch entity
$productGroup = $this->ProductGroups->patchEntity($productGroup, $this->request->data, [
'associated' => [
'StaffMembers' => ['onlyIds' => true],
'ProductIdentities' => ['onlyIds' => true]
]
]);
$this->request->data:
[
'group_name' => 'Creative and Performing Arts',
'active' => '1',
'product_type_id' => '2',
'staff_members' => [
'_ids' => [
(int) 0 => '103',
(int) 1 => '11',
(int) 2 => '3'
]
],
'product_identities' => [
'_ids' => [
(int) 0 => '1760',
(int) 1 => '1762',
(int) 2 => '1763',
(int) 3 => '1764'
]
]
]
edit.ctp
<?php
echo $this->Form->input('group_name',
['class' => 'form-control']);
echo $this->Form->input('active',
['class' => 'form-control']);
echo $this->Form->input('product_type_id',
['class' => 'form-control']);
echo $this->Form->input('staff_members._ids',
['options' => $staffMembers,
'class' => 'form-control height-200']);
echo $this->Form->input('product_identities._ids',
['options' => $productIdentities,
'class' => 'form-control height-500']);
?>
If a productGroup is not associated with any productIdentities, it saves fine. However if you want to select more productIdentities or unselect some existing ones, the data won't be saved. Instead, Cake will create new records in product_identites table.
After doing a debug, I could see that Cake did not patch entity correctly. See below:
'product_identities' => [
(int) 0 => object(App\Model\Entity\ProductIdentity) {
(int) 0 => '1760',
(int) 1 => '1762',
(int) 2 => '1763',
(int) 3 => '1764',
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
(int) 0 => true,
(int) 1 => true,
(int) 2 => true,
(int) 3 => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'ProductIdentities'
}
],
The onlyIds option is supposed to stop Cake from creating new records but obviously it didn't work.
And I've formatted my data into the format suggested in Cake's documentation so I'm very confused.
$data = [
'title' => 'My new article',
'body' => 'The text',
'user_id' => 1,
'comments' => [
'_ids' => [1, 2, 3, 4]
]
];
http://book.cakephp.org/3.0/en/orm/saving-data.html#converting-hasmany-data
Any comments/help is appreciated.