Hello,
I have a behavior problem when saving a belongsTo association.
In my project I have a missions table and a contacts table with :
// MissionsTable.php
$this->belongsTo('Contacts')
->setForeignKey('contact_id');
// missions
// - id
// - contact_id
// - ...
// contacts
// - id
// - firstname
// - lastname
// - email
// - ...
I would like to be able to add the contact and modify it if necessary. For example, to be able to add the contact’s email if it is not filled in on the existing entity. So I can’t just edit $mission->contact_id
My problem occurs when I edit a mission that has no contact and add an existing contact.
Saving will create a new contact entity despite the presence of the ID in the data.
// MissionsController.php
public function test($id)
{
$associated = ['Contacts'];
$mission = $this->Missions->get($id, contain: $associated);
// $mission->contact = null, because $mission->contact_id = null
$data = [
// ...
'contact' => [
'id' => '1',
'firstname' => 'John',
'lastname' => 'Doe',
'email' => 'john.doe@example.com',
]
];
$mission = $this->Missions->patchEntity($mission, $data, ['associated' => $associated]);
debug($mission);
die;
}
debug($mission)
result :
object(App\Model\Entity\Mission) id:0 {
'id' => (int) 1
...
'contact' => object(App\Model\Entity\Contact) id:1 {
'firstname' => 'John'
'lastname' => 'Doe'
'email' => 'john.doe@example.com'
'[new]' => true
...
}
'[new]' => false
}
We can see that after the patchEntity(), the contact is a new entity despite the presence of the id in the data.
I found some solutions to my problem, but I’m wondering if the behavior is normal or if there would be another way to not create a new entity with the id in the data.
Looking at the code of \Cake\ORM\Marshaller.php
- The ‘contact’ field launches the
_mergeAssociation()
callback - If the original is null (here,
if $mission->contact == null
)_mergeAssociation()
returns to_marshalAssociation()
, thenone()
because the association type is ‘manyToOne’. - In
one()
method, a new entity is created without checking the presence of the primary key in$data
.
Solution 1 : Edit the Core ORM Marshaller
In _mergeAssociation()
, shouldn’t we check for the presence of the primary key in $value before redirecting to _marshalAssociation()
which will create a new entity?
Or perhaps upstream, in _buildPropertyMap()
shouldn’t we check for the presence of the primary key in $value to set the $original?
// cakephp/src/ORM/Marshaller.php
protected function _buildPropertyMap(array $data, array $options): array
{
// ...
if (isset($options['isMerge'])) {
$callback = function (
$value,
EntityInterface $entity,
) use (
$assoc,
$nested,
): array|EntityInterface|null {
$options = $nested + ['associated' => [], 'association' => $assoc];
// edit part
if (isset($value[$assoc->getBindingKey()])) $original = $this->_table->{$assoc->getClassName()}->get($value[$assoc->getBindingKey()]);
else $original = $entity->get($assoc->getProperty());
return $this->_mergeAssociation($original, $assoc, $value, $options);
// end edit part
} else {
// ...
}
// ...
}
With the above modification, the behavior reacts well as I want.
Solution 2 : Edit associated entity in afterMarshal()
Otherwise, for now, what I can do is act in MissionsTable::afterMarshal()
Check for the presence of the id in $data and replace the contact entity.
But I need to do this for each belongsTo association.
// MissionsTable.php
public function afterMarshal(
EventInterface $event,
EntityInterface $entity,
ArrayObject $data,
ArrayObject $options
): void {
// belongsTo Contacts
if ($entity->isDirty('contact') && isset($data['contact']['id']) && !empty($data['contact']['id'])) {
// we do not create a new entity
$contact_entity = $this->Contacts->get($data['contact']['id']);
$contact_entity = $this->Contacts->patchEntity($contact_entity, $data['contact']);
$entity->contact = $contact_entity;
}
}
Thanks for reading