Cakephp 3 - Additional ID-field in belongsToMany association's through table - cakephp

I have been working on a CakePHP 3 project that has parts on it that are a bit more complex and it seems that the framework's official documentation does not give me the answer I am looking for. A following thing is needed to be done:
Users can belong to multiple Projects. In each Project each User that belongs to the current Project can access into different sections of the application, this is handled by a table called Resources, which stores the resource ids.
So in short, User A can have access to different amount of Resources in Project A and Project B.
I have setup following associations:
Users belongsToMany Projects through UsersProjects
Users belongsToMany Resources through UsersResources
In users_resources -table I have the following fields:
user_id | resource_id | project_id
project_id -field is added to the table to make the user's resources project-specific.
Association configuration is as follows:
UsersTable:
$this->belongsToMany('Resources', [
'foreignKey' => 'user_id',
'targetForeignKey' => 'resource_id',
'joinTable' => 'users_resources',
'through' => 'UsersResources'
]);
ResourcesTable:
$this->belongsToMany('Users', [
'foreignKey' => 'resource_id',
'targetForeignKey' => 'user_id',
'joinTable' => 'users_resources',
'through' => 'UsersResources'
]);
UsersResourcesTable:
$this->belongsTo('Users', [
'foreignKey' => 'user_id',
'joinType' => 'INNER'
]);
$this->belongsTo('Resources', [
'foreignKey' => 'resource_id',
'joinType' => 'INNER'
]);
$this->belongsTo('Projects', [
'foreignKey' => 'project_id',
'joinType' => 'INNER'
]);
There is also a buildRules-function implemented in the UsersResourcesTable-class to help with the data integrity:
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->existsIn(['user_id'], 'Users'));
$rules->add($rules->existsIn(['resource_id'], 'Resources'));
$rules->add($rules->existsIn(['project_id'], 'Projects'));
$rules->add($rules->isUnique(['user_id', 'resource_id', 'project_id']));
return $rules;
}
The data is saved like this:
$usersTable = TableRegistry::get('Users');
$user = $usersTable->get(129);
$data = [
'resources' => [
[
'id' => 48,
'_joinData' => [
'project_id' => 2
]
]
]
];
$user = $usersTable->patchEntity($user, $data, ['associated' => ['Resources']]);
if ($usersTable->save($user)) {
$this->out('success');
} else {
$this->out('fail');
}
The entity data after patchEntity:
{
"id": 129,
"email": "john.doe#example.com",
"created": null,
"modified": "2016-02-05T20:50:06+0000",
"firstname": "John",
"lastname": "Doe",
"type": 1,
"resources": [
{
"id": 48,
"type": "r",
"_joinData": {
"project_id": 2
}
}
]
}
When the data is saved with project id 2, the save works as expected. When I save the same data but I change the project id to 1 (user_id and resource_id will remain the same), the previous row with project id 2 is deleted from the table. The desired behaviour would obviously be that the older record would remain in the table. I noticed though that adding a saveStrategy-option with value 'append' to UsersTable's Resources-association definition will make the save go through correctly (the row with project_id 2 will not be deleted). But when I try to save an existing record while append-strategy is used, the save will fail (the save result is false).
So if I use replace-strategy, previous data gets deleted and if I use append-strategy, the update of an existing row will result to an error in cake's perspective. I think the error is thrown during rules checking due to the unique-rule.
I think the replace strategy would be the way to go but it seems that the project_id -field value gets ignored and it leads to the situation that previous data (with same user_id and resource_id) will be deleted whenever a new row is added with a different project_id.
So my question is:
Is it possible to add a third id-field to belongsToMany-association's trough-table so that the extra field would also be considered as an id-field so the framework would know when to update or insert records to the join-table?
The idea is that all the three fields in the table would be unique. Now it seems that the framework discards the extra field so inserts and updates do not work as expected. Or is there a design flaw in the database structure that is needed to be fixed?
I would prefer sticking with the CakePHP-conventions but it seems this scenario is quite hard to tackle. Any guidance is appreciated!

Related

CakePHP: to many belongsTo associations contained leads to DB running into JOIN limit

We are currently on CakePHP 3.7.5, using MariaDB as our database engine (10.4.12 on dev, 10.2, 10.2 on client machines).
Some of our table classes have a lot of belongsTo associations. Many of them pointing to a contacts table, but even more to an options table. We use this table to store arbitrary values for different properties of models so that the clients can manage these lists themselves, add entries or change their textual representation while keeping the referenced id.
Main Table: entities
id INT(11)
name VARCHAR(256)
administrator_contact_id INT(11)
account_manager_contact_id INT(11)
property_manager_contact_id INT(11)
...
type_id INT(11)
classification_id INT(11)
status_id INT(11)
category_id INT(11)
...
contacts table
id INT(11)
first_name VARCHAR(256)
last_name VARCHAR(256)
options table
id INT(11)
name VARCHAR(256)
EntitiesTable class
class EntitiesTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
$this->addAssociations([
'belongsTo' => [
'AdministratorContact' => [
'className' => 'Contacts',
],
'AccountManagerContact' => [
'className' => 'Contacts',
],
'PropertyManagerContact' => [
'className' => 'Contacts',
],
...
'Type' => [
'className' => 'Options',
],
'Classification' => [
'className' => 'Options',
],
'Status' => [
'className' => 'Options',
],
'Category' => [
'className' => 'Options',
],
...
],
...
]);
}
}
Example query in a Controller
$this->loadModel('Entities');
$query = $this->Entities->find('all', [
'conditions' => [
...
],
'contain' => [
'AdministratorContact',
'AccountManagerContact',
'PropertyManagerContact',
...
'Type',
'Classification',
'Status',
'Category',
....
],
]);
$results = $query->toArray();
Now, for the first time, we ran into MariaDBs limit of 61 JOINs. Our belongsTo associations are added using the default "join" strategy as it reduces the overall number of queries that have to be executed for serving a single request. Changing the strategy of all belongsTo associations to "select" would lead to way too many, unnecessary separate queries.
I was wondering if anybody heard of best practises, or whether there is a behavior that caters to this need. Things that come to my mind:
Before running a query, analyse how many associations are contained which would lead to JOINs, plus anything else that would lead to additional JOINs ("join" clause in the query, LinkableBehavior). If it exceeds the limit, turn some of them into separate SELECTs by amending their "strategy".
Somehow aggregate JOINS coming form belongsTo associations pointing to the same table by collecting the ids, and after the query has run "distribute" the results. Kind of what is done for hasMany associations, but there the results are turned into entities which are then all added as one property (containing an array of results), whereas for belongsTo we would then need to distribute the results back to different properties of the main model.

Two users associated to one transaction in CakePHP 3.x

In my app there is a Transaction table with:
seller_id, buyer_id and asset_id.
seller_id and buyer_id are id's supposed to point to Users table. To stick to the convention and keep automatic associations both should be called "user_id" (which is of course impossible)
What is the correct way to create such associations in CakePHP 3.x?
My guess is that I should create special association tables:
Sellers (id, user_id)
Buyers (id, user_id)
and then associations would be trough those tables:
Transaction => Sellers, Buyers => Users
Is that correct? Would it work? Is there a better way?
You can define the relationship with different alias and foreign keys like below.
In your transactions model/Table.
$this->belongsTo('Sellers' , [
'foreignKey' => 'seller_id',
'className' => 'Users'
]);
$this->belongsTo('Buyers' , [
'foreignKey' => 'buyer_id',
'className' => 'Users'
]);
If you also want to define the relationaship in user model, you can define this in this way.
In User model/table
$this->hasMany('BuyerTransactions' , [
'foreignKey' => 'buyer_id',
'className' => 'Transactions'
]);
$this->hasMany('SellerTransactions' , [
'foreignKey' => 'seller_id',
'className' => 'Transactions'
]);

CakePHP 3.x - Save many-to-many data

I am attempting to patchEntity with a join table, but I am unable to get it to save the associated records, and I think I have the backend code correct, but I am unsure of what the frontend code should look like...
Here is the scenario, I have three tables
Ledgers Table
id int
title string
Tribes Table
id int
name string
LedgersTribes Table
id int
ledger_id int
tribe_id int
Here is my backend code
public function ledgerSave($id = null)
{
$this->loadModel('Ledgers');
$ledger = $this->Ledgers->get($id, [
'contain' => ['LedgersTribes']
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$ledger = $this->Ledgers->patchEntity($ledger, $this->request->getData(), [
'associated' => ['LedgersTribes']
]);
if ($this->Ledgers->save($ledger)) {
$this->Flash->success(__('The ledger has been saved.'));
return $this->redirect($this->referer());
}
$this->Flash->error(__('The ledger could not be saved. Please, try again.'));
}
$this->set(compact('ledger'));
}
Here is the relevant frontend code
<?= $this->Form->control('tribe_id[]', ['type' => 'checkbox', 'label' => false, 'class' => 'minimal', 'value' => $tribe->id]) ?>
My question is, what should the field name be for the tribe_id, the idea is, i have a list of checkboxes and the user checks off a couple of boxes and then those tribe_id's get inserted into the LedgersTribes table with the ledger_id
Any ideas on how I can do this?
EDIT: Here is a screenshot of the form
I have reviewed the following links, and none of them answer my question...
CakePHP 3: Save Associated Model Fail
Save associated in cakephp 3 not working
How to save associated joinData in cakephp 3.x
CakePHP 3 cannot save associated model
Cakephp 3 - Save associated belongsToMany (joinTable)
This should do:
echo $this->Form->control('tribes._ids[]', [
'type' => 'checkbox',
'label' => false,
'class' => 'minimal',
'value' => $tribe->id]
]);
This is described here: https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-inputs-for-associated-data
I think you have to get the table (ex: TableRegistry::getTableLocator()->get('')) where you want to save the data. Then create entity from that and save the data, hopefully it will work.

CakePHP: Using attributesof model in associations-condition

I have a table that has a "hasOne"-association to another table, but without any foreign key. So my "hasOne"-association finds the related entity with a customized finder-method:
$this->hasOne('HourlyRates', [
'foreignKey' => false,
'finder' => ['byShift' => ['userId' => '1', 'shiftBegin' => '2016-01-01']]
]);
My problem is, that the options for the finder have to be the attributes of the actual entity of the first table. This is the user_id of one entity and the shift-begin of one entity.
I tried
'finder' => ['byShift' => ['userId' => 'Shifts.user_id', 'shiftBegin' => 'Shifts.begin']]
But it didn't work.
So do you know, how to access the attributes of one entity in the Table-class?

CakePHP 3.0 Timestamp behavior

I would like to update a timestamp in a users table when a user logs in. I created a datetime field called 'lastLogin'.
From my users controller's login action I call:
$user = $this->Auth->identify();
if ($user) {
$this->Auth->setUser($user);
$this->Users->touch($this->Users->get($user['id']), 'Users.afterLogin');
}
And in my Users table I have:
$this->addBehavior('Timestamp', [
'events' => [
'Model.beforeSave' => [
'created' => 'new',
'modified' => 'always',
],
'Users.afterLogin' => [
'lastLogin' => 'always'
]
]
]);
I have tested that the event is triggered and entity property is being updated. However it is not saved to the database.
Is this intended, i.e. do I have to explicitly save the entity, or am I missing something?
Thanks!
Andrius
The behavior only updates the field
It isn't really clear from the documentation, but the code only updates the field value. The behavior won't actually update the db, it's effectively assumed there would be a call to save later in the same request.

Resources