-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Description
Description
This will going to be a long one and I'm sorry for this upfront. I talked about this issue in the Slack channel for a very long time and I think it is a bug. Many thanks to all of you for your feedback and time! :)
I have 3 tables:
statuspages <1-----n> statuspages_to_statuspagegroups <n-----1> statuspagegroups
What I try to accomplish is to store multiple records into the statuspages_to_statuspagegroups with the same statuspagegroup_id and statuspage_id ID but different collection_id and category_id values.
To gain control over the join table, usually a through Table is used.
These are my associations:
class StatuspagegroupsTable extends Table {
public function initialize(array $config): void {
parent::initialize($config);
$this->setTable('statuspagegroups');
$this->setPrimaryKey('id');
$this->belongsToMany('Statuspages', [
'className' => 'Statuspages',
'through' => 'StatuspagesMembership',
'targetForeignKey' => 'statuspage_id',
'saveStrategy' => 'replace'
]);
}
}
class StatuspagesMembershipTable extends Table {
public function initialize(array $config): void {
$this->setTable('statuspages_to_statuspagegroups');
$this->setDisplayField('id');
$this->setPrimaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsTo('Statuspagegroups', [
'foreignKey' => 'statuspagegroup_id',
'joinType' => 'INNER',
]);
$this->belongsTo('Statuspages', [
'foreignKey' => 'statuspage_id',
'joinType' => 'INNER',
]);
}
}This association will work to read (find) data - no issues.
When inserting data (save), this association will only work as long as the statuspage_id is unique.
So I can insert data like to without any problems:
"statuspages": [
{
"id": 5, // <-- this works
"_joinData": {
"statuspagegroup_id": 5,
"collection_id": 10,
"category_id": 16,
"statuspage_id": 5 // <-- this works
}
},
{
"id": 1,
"_joinData": {
"statuspagegroup_id": 5,
"collection_id": 11,
"category_id": 16,
"statuspage_id": 1
}
},But, as soon as I try to create records with the same statuspage_id and statuspagegroup_id but different values for collection_id and category_id the ORM Marshaller will merge/overwrite the entities.
So i cannot save records like so:
"statuspages": [
{
"id": 1, // <-- this works not
"_joinData": {
"statuspagegroup_id": 5,
"collection_id": 10, // and i think bc cake ignores the values of collection_id and category_id
"category_id": 16,
"statuspage_id": 1 // <-- this works not
}
},
{
"id": 1,
"_joinData": {
"statuspagegroup_id": 5,
"collection_id": 11, // different collection_id and category_id than above
"category_id": 16,
"statuspage_id": 1
}
},I did not found a method to configure composite keys for the StatuspagesMembership association. I tried all sorts of combinations without any luck.
The ORM Marshaller will merge the two records (from request data) into one entity:
cakephp/src/ORM/Marshaller.php
Lines 672 to 690 in 0ee69b1
| public function mergeMany(iterable $entities, array $data, array $options = []): array | |
| { | |
| $primary = (array)$this->_table->getPrimaryKey(); | |
| $indexed = (new Collection($data)) | |
| ->groupBy(function ($el) use ($primary) { | |
| $keys = []; | |
| foreach ($primary as $key) { | |
| $keys[] = $el[$key] ?? ''; | |
| } | |
| return implode(';', $keys); | |
| }) | |
| ->map(function ($element, $key) { | |
| return $key === '' ? $element : $element[0]; | |
| }) | |
| ->toArray(); | |
| $new = $indexed[''] ?? []; |
For now the workaround @josbeir in the Slack channel came up with is, to manually create the records into the join table.
$statuspagegroup = $StatuspagegroupsTable->get($id);
/** @var StatuspagesMembershipTable $StatuspagesMembershipTable */
$StatuspagesMembershipTable = TableRegistry::getTableLocator()->get('StatuspagesMembership');
$StatuspagesMembershipTable->deleteAllRecordsByStatuspagegroupId($statuspagegroup->id);
$data = $this->request->getData(null, []);
if (empty($data['statuspages'])) {
$data['statuspages'] = [];
}
$joinTableRecords = [];
foreach ($data['statuspages'] as $statuspage) {
$joinTableRecords[] = Hash::remove($statuspage['_joinData'], 'id');
}
$joinTableEntities = $StatuspagesMembershipTable->newEntities($joinTableRecords);
$StatuspagesMembershipTable->saveMany($joinTableEntities);
foreach ($joinTableEntities as $entity) {
if ($entity->hasErrors()) {
$this->response = $this->response->withStatus(400);
$this->set('error', $entity->getErrors());
$this->viewBuilder()->setOption('serialize', ['error']);
return;
}
}While this workaround works, if feels wrong that i need to save the records manually.
The fact that collection_id and category_id are refs to other tables can be ignored as the same happens if you use strings like cat and dog for example.
CakePHP Version
5.2.6
PHP Version
8.3.6