Save fields in a jointable of a belongstomany association that don't belong to this association in cakephp 4.4

I have a belongstomany between two tables (Users and Groups, jointable GroupsUsers) this table has these fields: user_id, group_id, net_id, main_gate, created and modified), how can I save the net_id and main_gate fields in this table?
When I try to save it it gives me this warning: Creating default object from empty value.
This is the code for add action in UsersController:

    public function add()
    {
        $user = $this->Users->newEmptyEntity();
        if ($this->request->is('post')) {
			$image_file_name_url = $this->request->getData('image_file_name_url') ?? null;
			$type = $image_file_name_url->getClientMediaType() ?? null;
			if($type == "image/gif" || $type == "image/jpeg" || $type == "image/x-png" || empty($type)) {
				$size = $image_file_name_url->getSize() ?? 0;
				if($size < 2097152) {
					$user = $this->Users->patchEntity($user, $this->request->getData(), ['associated' => ['Groups._joinData']]);
					debug($user->groups);
					if(!empty($this->request->getData('groups.0._joinData'))){
						if(!empty($this->request->getData('groups.0._joinData.net_id'))) $user->groups[0[->_joinData->net_id = $this->request->getData('groups.0._joinData.net_id');
						if(!empty($this->request->getData('groups.0._joinData.main_gate'))) $user->groups[0]->_joinData->main_gate = $this->request->getData('groups.0._joinData.main_gate');
					}
					if(!empty($image_file_name_url)){
						$user->image_file_name_url = Router::url("/", true) . "upload/" . $this->modelClass . "/" . $image_file_name_url->getClientFilename();
						$user->image_file_name = WWW_ROOT . 'upload' . DS . $this->modelClass . DS . $image_file_name_url->getClientFilename();
						$user->image_file_name_filename = $image_file_name_url->getClientFilename();
					}
					if ($this->Users->save($user)) {
						if(!empty($image_file_name_url)) $image_file_name_url->moveTo(WWW_ROOT . 'upload' . DS . $this->modelClass . DS . $image_file_name_url->getClientFilename());
						$this->Flash->success(__('The user has been saved.'));

						$id = $this->Auth->user('id') ?? null;

						$action = (!empty($id)) ? 'index' : 'login';
						return $this->redirect(['action' => $action]);
					}
					$this->Flash->error(__('The user could not be saved. Please, try again.'));
				}else{
					$this->Flash->error(__('Debe seleccionar una image de 2 mb máximo.'));
				}
			}
        }
        $profiles = $this->Users->Profiles->find('list', ['limit' => 200])->all();
        $groups = $this->Users->Groups->find('list', ['limit' => 200])->all();
        $referrer = $this->Users->find('list', ['limit' => 200])->all();
        $this->set(compact('user', 'profiles', 'referrer', 'groups'));
    }

I retrieve the nets entity with this action:

    public function listNet()
	{
        if ($this->request->is('ajax')) {
			$this->autorender = false;
			$group_id = $this->request->getData('group_id');
			$nets  =  $this->Users->Groups->Nets->find('list')->where(['group_id' => $group_id]);
			$html = '<option value="0">- Seleccione una unidad funcional -</option>\n';
			foreach($nets as $key => $value) {
				$html .= '<option value="' . $key . '">' . $value . '</option>\n';
			}
			return $this->response->withStringBody($html);
		}
	}

And this is the code for add view:

<?php
/**
 * @var \App\View\AppView $this
 * @var \App\Model\Entity\User $user
 * @var \Cake\Collection\CollectionInterface|string[] $profiles
 * @var \Cake\Collection\CollectionInterface|string[] $groups
 */
?>
<div class="row">
    <aside class="column">
        <div class="side-nav">
            <h4 class="heading"><?= __('Acciones') ?></h4>
            <?= $this->Html->link(__('Listar Usuarios'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
        </div>
    </aside>
    <div class="column-responsive column-80">
        <div class="users form content">
            <?= $this->Form->create($user, ['type' => 'file']) ?>
            <fieldset>
                <legend><?= __('Agregar Usuario') ?></legend>
                <?php
                    echo $this->Form->control('username', ['label' => 'Nombre de usuario']);
                    echo $this->Form->control('password', ['label' => 'Contraseña']);
                    echo $this->Form->control('email', ['label' => 'E-mail']);
                    echo $this->Form->control('firstname', ['label' => 'Nombre']);
                    echo $this->Form->control('lastname', ['label' => 'Apellido']);
                    echo $this->Form->control('profile_id', ['label' => 'Perfil', 'options' => $profiles, 'empty' => '-- Seleccione un perfil --', 'id' => 'profileId']);
                    echo $this->Form->control('referrer_id', ['label' => 'Referente', 'options' => $referrer, 'empty' => '-- Seleccione un referente --']);
                    echo $this->Form->control('image_file_name_url', ['label' => 'Foto de perfil', 'type' => 'file']);
                    echo $this->Form->control('active', ['label' => 'Activo']);
					echo '<div id="selectGroup" style="display:none;">'.$this->Form->control('groups._ids', ['label' => 'Célula', 'options' => $groups, 'empty' => '-- Seleccione una célula --', 'id' => 'groupsId']).'</div>';
                    echo '<div id="selectNet" style="display:none;">'.$this->Form->control('groups.0._joinData.net_id', ['label' => 'Unidad funcional', 'id' => 'netId']).'</div>';
                    echo '<div id="selectGate" style="display:none;">'.$this->Form->control('groups.0._joinData.main_gate', ['label' => 'Nodo', 'options' => ['A' => 'A', 'B' => 'B'], 'empty' => '-- Seleccione un nodo --']).'</div>';
                ?>
            </fieldset>
            <?= $this->Form->button(__('Guardar')) ?>
            <?= $this->Form->end() ?>
        </div>
    </div>
</div>

<script>
$(function(){
	$('#profileId').change(function(){
		profile_id=$(this).val();
		switch (profile_id) {
			case "1": $('#selectGroup').hide();$('#selectNet').hide();$('#selectGate').hide();
				break;
			case "2": $('#selectGroup').hide();$('#selectNet').hide();$('#selectGate').hide();
				break;
			case "3": $('#selectGroup').show();$('#selectNet').hide();$('#selectGate').hide();
				$('#groupsId').prop('multiple', 'multiple');
				break;
			case "4": $('#selectGroup').show();$('#selectNet').show();$('#selectGate').hide();
				$('#groupsId').prop('multiple', false);
				break;
			case "5": $('#selectGroup').show();$('#selectNet').show();$('#selectGate').show();
				$('#groupsId').prop('multiple', false);
				break;
			case "6": $('#selectGroup').show();$('#selectNet').show();$('#selectGate').show();
				$('#groupsId').prop('multiple', false);
				break;
			case "7": $('#selectGroup').show();$('#selectNet').show();$('#selectGate').show();
				$('#groupsId').prop('multiple', false);
				break;
		}
	})
})
$(function(){
	$('#groupsId').change(function(){
		$.ajax({
			method:"POST", 
			url:"<?= $this->Url->build(['controller' => 'Users', 'action' => 'listNet']) ?>",
			data:{
				group_id:$(this).val()
			},
			success: function(data) {
				$('#netId').html(data);
			},
			headers:{
				'X-CSRF-Token':$('meta[name="csrfToken"]').attr('content')
			}
		})
	})
})
</script>

This is what is showing me debug($user->groups):

APP/Controller\UsersController.php (line 140)
[
  (int) 0 => object(App\Model\Entity\Group) id:0 {
    'id' => (int) 1
    'group_name' => 'Célula 1'
    'created' => object(Cake\I18n\FrozenTime) id:1 {
      'time' => '2023-03-23 12:03:57.000000-03:00'
      'timezone' => 'America/Argentina/Cordoba'
      'fixedNowTime' => false
    }
    'modified' => object(Cake\I18n\FrozenTime) id:2 {
      'time' => '2023-03-23 12:03:57.000000-03:00'
      'timezone' => 'America/Argentina/Cordoba'
      'fixedNowTime' => false
    }
    '[new]' => false
    '[accessible]' => [
      'group_name' => true,
      'created' => true,
      'modified' => true,
      'estadosx_dias' => true,
      'eventos' => true,
      'nets' => true,
      'pacients' => true,
      'pedidos' => true,
      'status_groups' => true,
      'stockx_users' => true,
      'turnos' => true,
      'users' => true,
    ]
    '[dirty]' => [
    ]
    '[original]' => [
    ]
    '[virtual]' => [
    ]
    '[hasErrors]' => false
    '[errors]' => [
    ]
    '[invalid]' => [
    ]
    '[repository]' => 'Groups'
  },
]

It doesn’t add the _joinData, do you know why is that happening?

You cannot use both the special _ids key and regular fields, the former will win over the latter when marshalling, resulting in any additional data being discarded.

If you want to combine a multi-select control and additional fields, then you have to transform the data before it is being marshalled, so that it the IDs are being converted into nested arrays with primary key fields, eg something like this:

'groups' => [
    '_ids' => [1, 2],
    [
        '_joinData' => [
            'net_id' => 1,
            'main_gate' => 'A',
        ],
    ]
]

needs to be converted to something like this:

'groups' => [
    [
        'id' => 1,
    ],
    [
        'id' => 2,
    ],
    [
        '_joinData' => [
            'net_id' => 1,
            'main_gate' => 'A',
        ],
    ]
]

https://book.cakephp.org/4/en/orm/saving-data.html#converting-belongstomany-data

You could for example use the beforeMarshal callback in your UsersTable class for that:

public function beforeMarshal(\Cake\Event\EventInterface $event, \ArrayObject $data, \ArrayObject $options)
{
    if (isset($data['groups']['_ids'])) {
        foreach ($data['groups']['_ids'] as $id) {
            $data['groups'][] = ['id' => $id];
        }
        unset($data['groups']['_ids']);
    }
}

https://book.cakephp.org/4/en/orm/saving-data.html#modifying-request-data-before-building-entities

How do I have to put the onlyIds=>true and the joindata in the patchentity method?
This is the code for the patchentity in add action:

$userData = $this->request->getData();
$user = $this->Users->patchEntity($user, $userData, [
	'associated' => [
		'Groups' => [
			'onlyIds' => true,
			'_joinData' => [
				'net_id' => $userData['groups'][0]['_joinData']['net_id'] ?? null,
				'main_gate' => $userData['groups'][0]['_joinData']['main_gate'] ?? null
			]
		]
	]
]);

What am I doing wrong? It gives me an exception strpos() expects parameter 1 to be string, array given in ‘Groups’ => [‘onlyIds’ => true]

What makes you think that this would be supported? What did even you expected it to do? And why did you not stick with your initial patching code that would’ve worked fine when fed with the properly structured data?

I want to save the joindata but don’t want to add records to Groups table, I only want to add records to GroupsUsers table but the net_id field it doesn’t save any value and the joindata doesn’t appear in the user->groups entity when I do a debug, I am new to cakephp I only have two years using it

This is what it shows debug($user->groups):

APP/Controller\UsersController.php (line 159)
object(App\Model\Entity\Group) id:0 {
  'id' => (int) 1
  'group_name' => 'Célula 1'
  'created' => object(Cake\I18n\FrozenTime) id:1 {
    'time' => '2023-03-23 12:03:57.000000-03:00'
    'timezone' => 'America/Argentina/Cordoba'
    'fixedNowTime' => false
  }
  'modified' => object(Cake\I18n\FrozenTime) id:2 {
    'time' => '2023-03-23 12:03:57.000000-03:00'
    'timezone' => 'America/Argentina/Cordoba'
    'fixedNowTime' => false
  }
  '[new]' => false
  '[accessible]' => [
    'group_name' => true,
    'created' => true,
    'modified' => true,
    'statesx_days' => true,
    'events' => true,
    'nets' => true,
    'pacients' => true,
    'orders' => true,
    'status_groups' => true,
    'stocks_users' => true,
    'turns' => true,
    'users' => true,
  ]
  '[dirty]' => [
  ]
  '[original]' => [
  ]
  '[virtual]' => [
  ]
  '[hasErrors]' => false
  '[errors]' => [
  ]
  '[invalid]' => [
  ]
  '[repository]' => 'Groups'
}

I don’t know why it doesn’t add the joindata.

Because the _joinData needs to be in the data you’re patching into the entity, not in the associated array. That’s for telling the system how to handle the association, not anything about the data.

I created the joindata entity in the user after patchentity, but it still isn’t saving the net_id, what am I doing wrong?
This is the code where I create de joindata entity:

if(!empty($userData['groups'][0]['_joinData'])){
	$net_id = $userData['groups'][0]['_joinData']['net_id'] ?? null;
	$main_gate = $userData['groups'][0]['_joinData']['main_gate'] ?? null;
	$user->_joinData = new Entity(['net_id' => $net_id, 'main_gate' => $main_gate], ['markNew' => true]);
}

And this is what it shows debug($userData):

APP/Controller\UsersController.php (line 154)
[
  'username' => 'alejandro.ambrosini@unc.edu.ar',
  'password' => 'lytfvn24108508',
  'email' => 'alejandro.ambrosini@unc.edu.ar',
  'firstname' => 'Alejandro',
  'lastname' => 'Ambrosini',
  'profile_id' => '4',
  'referrer_id' => '3',
  'active' => '1',
  'groups' => [
    '_ids' => [
      (int) 0 => '1',
    ],
    (int) 0 => [
      '_joinData' => [
        'net_id' => '1',
        'main_gate' => '',
      ],
    ],
  ],
  'image_file_name_url' => object(Laminas\Diactoros\UploadedFile) id:0 {
    private clientFilename => 'ale_original.jpg'
    private clientMediaType => 'image/jpeg'
    private error => (int) 0
    private file => 'D:\wamp64\tmp\phpC529.tmp'
    private moved => false
    private size => (int) 25034
    private stream => null
  },
]