saving model with deep association belonging to both higher models - cakephp

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']);
}
}

Related

How to create a self BelongsToMany relation in CakePHP 3

I have a database with a table "projekte" (german word for projects).
Some of the projects have a relation to eachother.
So i would like to have a BTM relation.
I created a joinTable "projektverbindungen" with the following fields:
projekt_id
nebenprojekt_id
I found a similar question here: BelongstoMany relationship between a table and itself and i tried the answer of ndm, but without success.
Here is my ProjekteTable.php
class ProjekteTable extends Table {
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('projekte');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->hasOne('Projekteigenschaften', [
'foreignKey' => 'projekt_id',
'dependent' => true,
]);
$this->belongsToMany('Projekte', [
'foreignKey' => 'projekt_id',
'targetForeignKey' => 'nebenprojekt_id',
'joinTable' => 'projektverbindungen',
]);
}
}
Here ist my template (add.ctp)
<?php
echo $this->Form->create($projekt);
echo $this->Form->control('name', ['class' => 'form-control']);
echo $this->Form->control('projekteigenschaft.projektverantwortlich');
echo $this->Form->control('projekteigenschaft.beschreibung');
echo $this->Form->control('projekte._ids', ['options' => $projekte, 'multiple' => true]);
echo $this->Form->button(__('Submit'));
echo $this->Form->end();
?>
The first step, saving a project with a related project works as expected.
The id of the created project was saved as projektverbindungen.projekt_id and the id of the related project as projektverbindungen.nebenprojekt_id.
When i query a projekt without the relation to other projects like so:
$projekt = $this->Projekte->get($id, [
'contain' => ['Projekteigenschaften']
]);
the query looks like this:
SELECT Projekte.id AS `Projekte__id`, Projekte.name AS `Projekte__name`, Projekteigenschaften.id AS `Projekteigenschaften__id`, Projekteigenschaften.projektverantwortlich AS `Projekteigenschaften__projektverantwortlich`, Projekteigenschaften.beschreibung AS `Projekteigenschaften__beschreibung`, Projekteigenschaften.projekt_id AS `Projekteigenschaften__projekt_id` FROM projekte Projekte LEFT JOIN projekteigenschaften Projekteigenschaften ON Projekte.id = (Projekteigenschaften.projekt_id) WHERE (Projekte.id = :c0 AND (Projekte.deleted) IS NULL)
And the debug of the result looks like:
"id": "6862279f-8134-401f-86ff-9278a3bfa5c3",
"name": "My Project",
"projekteigenschaft": {
"id": "89d9e241-e700-4c31-9266-ee5717f2a0aa",
"projektverantwortlich": "Blisginnis, Ralf",
"beschreibung": ""
}
Everything works fine.
But when i add the projects to contain like so:
$projekt = $this->Projekte->get($id, [
'contain' => ['Projekteigenschaften', 'Projekte']
]);
The query looks the same like above, but the entity looks a bit different:
"Projekteigenschaften": {
"id": "89d9e241-e700-4c31-9266-ee5717f2a0aa",
"projektverantwortlich": "Blisginnis, Ralf",
"beschreibung": ""
}
Projekteigenschaften seems no longer to be a hasOne relation and "Projekte" gets totally ignored.
Anyone has an idea what i did wrong? Or should i prefer an other way of doing this?
edit after ndm´s comment
I tried defining the relationship like so:
$this->belongsToMany('Projektverbindungen', [
'class' => 'Projekte',
'foreignKey' => 'projekt_id',
'targetForeignKey' => 'nebenprojekt_id',
'joinTable' => 'projektverbindungen',
]);
and changed the add.ctp template like so:
echo $this->Form->control('projektverbindungen._ids', ['options' => $projekte, 'multiple' => true]);
But then it doesn´t save the relation.
I also tried to rename the joinTable to projekte_projekte. It didn´t seem to make any difference.
Then I tried to use the through-option, but the results of that were even worse.
So I continued trying to find a solution with the method described above.
2nd edit
projekverbindungen ist accessible in Projekt.php:
protected $_accessible = [
'name' => true,
'projekteigenschaft' => true,
'projekte' => true,
'projektverbindungen' => true,
];
Debug of requestData:
[
'name' => 'My Project',
'projekteigenschaft' => [
'projektverantwortlich' => 'John Doe',
'beschreibung' => '',
'projektverbindungen' => [
'_ids' => [
(int) 0 => '809031f2-4ecd-4dfb-82d5-2c911286dd21'
]
]
]
Debug of entity after patching:
object(App\Model\Entity\Projekt) {
'name' => 'My Project',
'projekteigenschaft' => object(App\Model\Entity\Projekteigenschaft) {
'projektverantwortlich' => 'John Doe',
'beschreibung' => '',
'[new]' => true,
'[accessible]' => [
'projektverantwortlich' => true,
'beschreibung' => true,
'projekt_id' => true,
'projekt' => true
],
'[dirty]' => [
'projektverantwortlich' => true,
'beschreibung' => true
],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Projekteigenschaften'
},
'projektverbindungen' => [],
'[new]' => true,
'[accessible]' => [
'name' => true,
'projekteigenschaft' => true,
'projekte' => true,
'projektverbindungen' => true
],
'[dirty]' => [
'name' => true,
'projekteigenschaft' => true,
'projektverbindungen' => true
],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Projekte'
}
3rd edit
In my bootstrap.php i have this:
Inflector::rules('plural', [
'/^(projekt)$/i' => '\1e',
'/^(projekteigenschaft|projektverbindung)$/i' => '\1en',
]);
Inflector::rules('singular', [
'/^(projekt)e$/i' => '\1',
'/^(projekteigenschaft|projektverbindung)en$/i' => '\1',
]);
After your recommendation I additionally added propertyName to the definition of the association:
$this->belongsToMany('Projektverbindungen', [
'class' => 'Projekte',
'propertyName' => 'Projektverbindungen',
'foreignKey' => 'projekt_id',
'targetForeignKey' => 'nebenprojekt_id',
'joinTable' => 'projektverbindungen',
]);
After that, the patched entity looks like this:
object(App\Model\Entity\Projekt) {
'name' => 'My Project',
'projekteigenschaft' => object(App\Model\Entity\Projekteigenschaft) {
'projektverantwortlich' => 'John Doe',
'beschreibung' => '',
'[new]' => true,
'[accessible]' => [
'projektverantwortlich' => true,
'beschreibung' => true,
'projekt_id' => true,
'projekt' => true
],
'[dirty]' => [
'projektverantwortlich' => true,
'beschreibung' => true
],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Projekteigenschaften'
},
'projektverbindungen' => [
'_ids' => [
(int) 0 => '1e28a3d1-c914-44be-b821-0e87d69cd95f'
]
],
'[new]' => true,
'[accessible]' => [
'name' => true,
'projekteigenschaft' => true,
'projekte' => true,
'projektverbindungen' => true
],
'[dirty]' => [
'name' => true,
'projekteigenschaft' => true,
'projektverbindungen' => true
],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Projekte'
}
But still no new entry in the table "projektverbindungen"
The last suggestion of ndm made the trick.
Now it works like expected. Thank you very much!
Here is the correct setup:
ProjekteTable.php
$this->belongsToMany('Nebenprojekte', [
'className' => 'Projekte',
'foreignKey' => 'projekt_id',
'targetForeignKey' => 'nebenprojekt_id',
'joinTable' => 'projektverbindungen',
]);
Here I used the property class instead of className, that has been the biggest issue.
Thats really embarrassing, because in the Cookbook is the correct name of that property:
https://book.cakephp.org/3/en/orm/associations.html#belongstomany-associations
Nevertheless, perhaps anyone else makes the same mistake and this thread can help.
The second thing is not to use the jointable`s name as the name of the association.
The rest is just straight forward...
Making the association accessible in the Entity Class (Projekt.php):
protected $_accessible = [
'name' => true,
'projekteigenschaft' => true,
'nebenprojekte' => true,
];
ProjekteController.php ("add" and "edit"):
public function add()
{
$projekt = $this->Projekte->newEntity();
if ($this->request->is('post')) {
$projekt = $this->Projekte->patchEntity($projekt, $this->request->getData());
if ($this->Projekte->save($projekt)) {
$this->Flash->success(__('flash message'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('error message'));
}
$projekte = $this->Projekte->find('list');
$this->set(compact('projekt', 'projekte'));
}
public function edit($id = null)
{
$projekt = $this->Projekte->get($id, [
'contain' => ['Projekteigenschaften', 'Nebenprojekte']
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$projekt = $this->Projekte->patchEntity($projekt, $this->request->getData());
if ($this->Projekte->save($projekt)) {
$this->Flash->success(__('flash message'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('error message'));
}
$projekte = $this->Projekte->find('list')->where(['id !=' => $id]);
$this->set(compact('projekt', 'projekte'));
}
In the templates like add.ctp or edit.ctp:
echo $this->Form->control('nebenprojekte._ids', ['options' => $projekte, 'multiple' => true]);
If you use another language than english, don´t forget to set the correct inflection rules.
bootstrap.php:
Inflector::rules('plural', [
'/^(projekt|nebenprojekt)$/i' => '\1e',
'/^(projekteigenschaft)$/i' => '\1en',
]);
Inflector::rules('singular', [
'/^(projekt|nebenprojekt)e$/i' => '\1',
'/^(projekteigenschaft)en$/i' => '\1',
]);

Controller doesn't get hasOne relationship data - Cakephp

I have a problem with related tables in CakePHP. I can't get the related table data include in the form.
I have two Entities. One of them is "Users" and the other one is "Subjects". Every User has a subject. Table "Subject" has foreign key idUser from Users table.
I added in UsersTable:
$this->hasOne('Subjects');
And I added in SubjectsTable:
$this->belongsTo('Users', [
'foreignKey' => 'idUser',
'joinType' => 'INNER'
]);
In the view (signup), I have this:
<div class="form-group">
<?php echo $this->Form->control('Subject.name',['label' => 'Asignatura','placeholder' => 'Ingrese asignatura','class' => 'form-control']) ?>
</div>
In the controller, I have this:
$user = $this->Users->patchEntity($user, $this->request->getData(),['associated' => 'Subjects']);
When I debug $user, I am getting this result:
\src\Controller\UsersController.php (line 113)
object(App\Model\Entity\User) {
'id' => '11111111',
'name' => 'Leo',
'firstlastname' => 'Messi',
'secondlastname' => 'Cuccittini',
'email' => 'leo.messi#gmail.com',
'password' => '$2y$10$E02nd/w89BDvgCyz36bQdeBbujOLrSdON1e6CD25aDYCP2VeLkNNm',
'role' => '2',
'[new]' => true,
'[accessible]' => [
'id' => true,
'name' => true,
'firstlastname' => true,
'secondlastname' => true,
'email' => true,
'password' => true,
'role' => true
],
'[dirty]' => [
'id' => true,
'name' => true,
'firstlastname' => true,
'secondlastname' => true,
'email' => true,
'password' => true,
'role' => true
],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Users'
}
So, I am not getting in the controller the data from Subject.
Any help, please.
Model
$this->hasOne('Subjects', [
'foreignKey' => 'userId'
]);
Controller:
$user = $this->User->get($id, ['contain' => ['Subjects']);
Entity/User.php
protected $_accessible = [
'subjects' => true
// ...
];
Form
https://book.cakephp.org/3.0/en/views/helpers/form.html#associated-form-inputs
Change: Subject.name to user.subject.name
<?php echo $this->Form->control('user.subject.name',['label' => 'Asignatura','placeholder' => 'Ingrese asignatura','class' => 'form-control']) ?>

saving belongsToMany association with same keys but different _joinData

I have a belongsToMany relationship between two tables which is configured using a through table.
class UsersTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Groups', [
'through' => 'GroupsUsers',
]);
}
}
class GroupsTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Users', [
'through' => 'GroupsUsers',
]);
}
}
class GroupsUsersTable extends Table
{
public function initialize(array $config)
{
$this->belongsTo('Groups');
$this->belongsTo('Users');
}
}
I can make associations between users and groups, but not I can't figure out how to make multiple associates between the same user and groups, but with different _joinData.
This is how the join table is built:
$table = $this->table('groups_users');
$table->addColumn('user_id', 'integer');
$table->addColumn('group_id', 'integer');
$table->addColumn('role', 'string');
$table->create();
When I add or edit a group by adding users to it, the $data in beforeMarshal() is
object(ArrayObject) {
name => 'test group'
users => [
(int) 0 => [
'id' => (int) 1,
'_joinData' => [
'role' => 'writer'
]
],
(int) 1 => [
'id' => (int) 1,
'_joinData' => [
'role' => 'editor'
]
]
]
}
after patching the entity in the controller with
$entity = $this->Groups->patchEntity(
$entity,
$data,
['associated' => ['Users._joinData']]
);
I get the following $entity in beforeSave()
object(App\Model\Entity\Group) {
'id' => (int) 1,
'name' => 'test group',
'users' => [
(int) 0 => object(App\Model\Entity\User) {
'id' => (int) 1,
'username' => 'testuser',
'_joinData' => object(Cake\ORM\Entity) {
'role' => 'writer',
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'role' => true
],
'[original]' => [],
'[virtual]' => [],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'GroupsUsers'
},
'[new]' => false,
'[accessible]' => [
'username' => true,
'groups' => true,
'_joinData' => true
],
'[dirty]' => [
'_joinData' => true
],
'[original]' => [
'_joinData' => [
'role' => 'writer'
]
],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Users'
}
],
'[new]' => false,
'[accessible]' => [
'name' => true,
'users' => true
],
'[dirty]' => [
'users' => true,
'modified' => true
],
'[original]' => [
'modified' => object(Cake\I18n\FrozenTime) {
'time' => '2019-05-15T14:41:49+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
}
],
'[hasErrors]' => false,
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Groups'
}
So it looks like the Marshaller is removing the second "duplicate" user from the group, even though the _joinData is different. The default saveStrategy is replace, but manually changing it to append doesn't add the second association either.
How can I add the same user to the same group with different _joinData using the same join table.
The marshaller uses the primary key to identify records, and the collection of existing entities will then only hold one user entity instance, so even though the marshaller will add the different join data sets, it will set it on one and the same entity... long story short, the marshaller isn't (yet) capable of doing what you're trying to do. You may want to open an issue over at GitHub if there isn't one already.
For now you'll have to save the records separately, for example like this (untested, but you get the idea):
$group = $this->Groups->patchEntity($entity, $data, [
'fieldList' => ['name']
]);
$users = [];
foreach ($data['users'] as $userData) {
$user = $this->Groups->Users->newEntity();
$users[] = $this->Groups->Users->patchEntity($user, $userData);
}
$result = $this->Groups->getConnection()->transactional(function () use ($group, $users) {
if (!$this->Groups->save($group, ['atomic' => false])) {
return false;
}
if (!$this->Groups->Users->link($group, $users, ['atomic' => false])) {
return false;
}
return true;
});

Cakephp 3 - belongsToMany with _joinData

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).

cakephp 3.x Saving Nested (deep) Association

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

Resources