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.
Related
I'm developing with CakePHP 4 and try to save a nested entity.
The new data are looking like that:
$data = [
'device_status_id' => '3',
'name' => 'myDevice',
'modules' => [
[
'name' => 'Autodetect',
'module_state_id' => '1',
'module_class_id' => '12',
'module_type_id' => '4',
'ports' => [
[
'physical_port' => '1',
'port_unit_id' => '1',
'port_identity' => '201901',
'name' => 'Analog-1',
],
[
'physical_port' => '2',
'port_unit_id' => '1',
'port_identity' => '201902',
'name' => 'Analog-2',
],
],
],
]
];
The structure is looking like that: $device -> hasMany($modules) -> hasMany($ports)
The table associations with hasMany are also given and working:
e.g.
$this->hasMany('Ports', [
'foreignKey' => 'module_id',
'dependent' => true,
'cascadeCallbacks' => true,
]);
The same for device as well...
In my controller I'm doing the following:
$device = $this->Devices->newEmptyEntity();
$device->setDirty('modules', true);
$device->setDirty('ports', true); // right?
// adding more info to $device
if ($this->request->is('post')) {
$device = $this->Devices->patchEntity($device, $this->request->getData(), [
'validate' => false,
'associated' => [
'DeviceTypes',
'DeviceStatuses',
'Modules',
'Modules.ModuleStates',
'Modules.ModuleClasses',
'Modules.ModuleTypes',
'Modules.Ports',
]
]);
$this->Devices->save($device)
Now the save fails, as the validator is complaining about a missing port->id.
So what is wrong here? The modules are created in the DB, but not the associated ports:
[
'device' => [
'modules' => [
(int) 0 => [
'ports' => [
(int) 0 => [
'id' => [
'_required' => 'This field is required',
],
....
Many thanks for your hints how I can force the ports to be added to the DB.
I have three tables, Articles, Comments, and Tags.
Tags belong to both Articles and Comments.
$this->Articles->patchEntity($entity, $this->request->getData(), [
'associated' => ['Comments.Tags']
]);
with the following error:
SQLSTATE[HY000]: General error: 1364 Field 'article_id' doesn't have a default value
Please try correcting the issue for the following table aliases:
CommentsArticles
but if I save with only 'associated' => ['Comments'] it works saving the Article and Comments with join table associations, just doesn't save any Tags.
Articles table has these associations:
$this->hasMany('Tags', [
'foreignKey' => 'article_id'
]);
$this->belongsToMany('Comments', [
'foreignKey' => 'article_id',
'targetForeignKey' => 'comment_id',
'joinTable' => 'comments_articles'
]);
Comments table has these associations:
$this->hasMany('Tags', [
'foreignKey' => 'comment_id'
]);
$this->belongsToMany('Articles', [
'foreignKey' => 'comment_id',
'targetForeignKey' => 'article_id',
'joinTable' => 'comments_articles'
]);
and Tags table has these associations:
$this->belongsTo('Comments', [
'foreignKey' => 'comment_id',
'joinType' => 'INNER'
]);
$this->belongsTo('Articles', [
'foreignKey' => 'article_id',
'joinType' => 'INNER'
]);
This is the entity after patching looks like this.
object(App\Model\Entity\Article) {
'title' => 'example article name',
'users' => [
'_ids' => []
],
'comments' => [
(int) 0 => object(App\Model\Entity\Comment) {
'id' => (int) 1,
'content' => 'this is a comment',
'tags' => [
(int) 0 => object(App\Model\Entity\Tag) {
'name' => 'example tag name',
'[new]' => true,
'[accessible]' => [
'comment_id' => true,
'article_id' => true,
'comment' => true,
'article' => true
],
'[dirty]' => [
'name' => true
],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Tags'
}
],
'[new]' => false,
'[accessible]' => [
'content' => true,
'tags' => true,
'articles' => true
],
'[dirty]' => [
'tags' => true
],
'[original]' => [
'tags' => [
(int) 0 => [
'name' => '0'
]
]
],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Comments'
}
],
'[new]' => true,
'[accessible]' => [
'title' => true,
'tags' => true,
'comments' => true,
'users' => true
],
'[dirty]' => [
'title' => true,
'users' => true,
'comments' => true
],
'[original]' => [
'users' => []
],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Articles'
}
CaekPHP doesn't support that, it can only populate foreign keys of direct associations / in one direction. You could for example:
prepopulate the foreign key fields (which will of course only work when the article and/or comment already exists)
manually save the tags separately using the primary keys of the article and comment records
create association classes that pass the article primary key into the options when saving the article, and uses that to populate the article_id field when saving the tag
hook into the saving process on table level to pass on the article primary key and populate the tags with it
Here's a quick and dirty example for the latter solution, which should also give you an idea on how it could work on association level:
In ArticlesTable:
public function beforeSave(
\Cake\Event\Event $event,
\Cake\Datasource\EntityInterface $entity,
\ArrayObject $options
) {
if (isset($options['Articles.id'])) {
unset($options['Articles.id']);
}
}
protected function _onSaveSuccess($entity, $options)
{
if ($options['_primary']) {
$options['Articles.id'] = $entity->get('id');
}
return parent::_onSaveSuccess($entity, $options);
}
In TagsTable:
public function beforeSave(
\Cake\Event\Event $event,
\Cake\Datasource\EntityInterface $entity,
\ArrayObject $options
) {
if (!$options['_primary'] &&
isset($options['Articles.id'])
) {
$entity->set('article_id', $options['Articles.id']);
}
}
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).
I have product data coming from a 3rd party service call that I then create an object from and save to my MySQL DB. My models are as follows:
'products' hasMany>> 'product_skus' hasMany>> 'product_sku_attributes'
table relationships
In my ProductsTable.php initialize() method I have:
$this->hasMany('ProductSkus', [
'foreignKey' => 'product_no',
'dependent' => true,
]);
In my ProductSkusTable.php initialize() method I have:
$this->hasMany('ProductSkuAttributes', [
'foreignKey' => 'product_sku_id',
'bindingKey' => 'id',
'propertyName' => 'product_sku_attributes',
'dependent' => true,
]);
My controller:
$products = TableRegistry::get('Products');
$entity = $products->newEntity($product_data[0]);
$products->save($entity, [
'associated' => [
'ProductSkus',
'ProductSkus.ProductSkuAttributes',
]
]);
Here's is the relevant snippet from my entity debug:
'product_skus' => [
(int) 0 => object(App\Model\Entity\ProductSkus) {
'sku' => 'BDS1401H',
'sku_price' => (float) 366.76,
'sku_weight' => (float) 38.1,
'sku_img_main' => '',
'sku_img_large' => '',
'sku_img_default' => false,
'is_default' => true,
'product_sku_attributes' => [
(int) 0 => [
'product_no' => (int) 23200,
'sku' => 'BDS1401H',
'attribute_name' => 'Front Sway Bar Links',
'option_name' => 'Stock'
],
(int) 1 => [
'product_no' => (int) 23200,
'sku' => 'BDS1401H',
'attribute_name' => 'Shock Options',
'option_name' => 'NX2 Series'
],
(int) 2 => [
'product_no' => (int) 23200,
'sku' => 'BDS1401H',
'attribute_name' => 'Steering Stabilizer Options',
'option_name' => 'Stock'
]
],
'[new]' => true,
'[accessible]' => [
'*' => true,
'id' => true
],
'[dirty]' => [
'sku' => true,
'sku_price' => true,
'sku_weight' => true,
'sku_img_main' => true,
'sku_img_large' => true,
'sku_img_default' => true,
'is_default' => true,
'product_sku_attributes' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'ProductSkus'
},
(int) 1 => object(App\Model\Entity\ProductSkus) { ...
I doubled checked, and all my fields are set as accessible in my table entity classes. Also, at this point I'm only trying to save one product record for simplicity, hence $products->newEntity().
My data is saving to 'products' and 'product_skus' tables without problem, but not to 'product_sku_products'. Can anyone see what the problem is? Is it because I'm not using the same foreignKey?
Please let me know what else I can provide for clarity.
The product_sku_attributes data is not being marshalled, it's still an array of arrays, and not an array of entities, hence it's not being saved.
Just like when saving entities, creating/patching them with associated data by default only works for first level associations. Deeper nested associations require to specify them via the associated option, ie:
$entity = $products->newEntity($product_data[0], [
'associated' => [
'ProductSkus.ProductSkuAttributes'
]
]);
$products->save($entity, [
'associated' => [
'ProductSkus.ProductSkuAttributes'
]
]);
See also
Cookbook > Database Access & ORM > Saving Data > Converting Request Data into Entities
Cookbook > Database Access & ORM > Saving Data > Saving Associations
I have printed object and trying to print Tenats.stage
<?php foreach ($tenancies as $tenancy): ?>
<td><?= debug($tenancy); ?></td>
Print this =>
object(App\Model\Entity\Tenancy) {
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2016-03-18T15:57:40+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'tenants' => [],
'property' => object(Cake\ORM\Entity) {
'id' => (int) 4110,
'address1' => '119 Alan Moss Road',
'postcode' => 'le115ly',
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Properties'
},
'_matchingData' => [
'Tenants' => object(Cake\ORM\Entity) {
'stage' => (int) 2,
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Tenants'
}
],
I need to 'stage' value
Any help please
That's how to print _matchingData
<td><?= h($tenancy->_matchingData['Tenants']->stage); ?></td>
But if you specify the main parents field id (Tenancy.id) automaticaly your data will look much much better. for example my parent model is "Tenancy" Now I am getting Tenant.id and Tenancy.id and Property.id :
$tenancies = $this
->find()
->select([
'Tenancy.id', 'Tenancy.created', 'Tenancy.stage',
'Properties.id', 'Properties.address1', 'Properties.postcode',
'Tenants.stage',
])
->contain('Properties', function(\Cake\ORM\Query $query) {
return $query->where([
'Properties.active' => 1
]);
})
->contain([
'Tenants'
])
->matching('Tenants', function(\Cake\ORM\Query $query) {
return $query->where([
'Tenants.active' => 1
]);
})
->where([
'Tenancy.active' => 1,
$conditions
])
->order([
'Tenancy.created' => 'DESC',
'Tenants.tenancy_id'
]);
return $tenancies;
}
It prints the array with deep associations which is cool and I can get my tenants property like this:
<td><?= h($tenancy->tenants->stage); ?></td>
Prints=>
0 => object(App\Model\Entity\Tenancy) {
'id' => (int) 3923,
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2016-03-19T13:12:32+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'stage' => (int) 2,
'tenants' => [
(int) 0 => object(Cake\ORM\Entity) {
'id' => (int) 8903,
'user_id' => (int) 15318,
'tenancy_id' => (int) 3923,
'needs_guarantor' => true,
'guarantor_id' => null,
'holding_fee' => (float) 50,
Result: Make sure always you write your query properly so you access your data nice and tidy.