This issue seems pretty basic to me, so I would like to know where it goes wrong.
I will show my problem through a simple example. I create two tables with a simple belongsTo relation.
create table philosophers (
id int unsigned primary key auto_increment,
full_name varchar(255) not null unique
);
create table books (
id int unsigned primary key auto_increment,
title varchar(255) unique not null,
philosopher_id int unsigned not null,
foreign key `philosopher_id` (philosopher_id) references philosophers(id)
)
And bake everything on a fresh cakePHP 3.4.8 installation. So far so good. Here's the catch:
I would like to write the Philosopher's name in a text box, and have CakePHP associate it with an existing name, if it is existing, or add a new one, if it is not yet existing. So, according to the conventions, I replace
echo $this->Form->control('philosopher_id', ['options' => $philosophers]);
in file src/Template/Books/add.ctp, with:
echo $this->Form->control('philosopher.full_name');
In the second case (adding a new entry), it works brilliantly, adding the foreign keys and all.
In order to achieve the first option, I have tried
- Implicitly setting
'checkExisting'
for the associated table in the$entity->save()
phase. - Making the
id
accessible in thePhilosopher
entity. - Creating a Behavior that adds the id in the
beforeMarshal
event.
This is the Behavior:
SEE BELOW
It just doesn't seem to want to create the existing entity. I know that I can do what it says here, but this practically bypasses validation entirely.
I am almost certain that I am missing something... Wish I knew what it is.
EDIT: I updated and corrected the Behavior, taking @ndm 's solution into account.
namespace App\Model\Behavior;
use Cake\ORM\Behavior;
use Cake\Event\Event;
use ArrayObject;
use Cake\ORM\TableRegistry;
use Cake\Utility\Inflector;
/**
* This class prevents the belongsTo relation from
* always creating new entries, by modifying the data
* before it is marshalled.
*
* The config should have an entry called 'fields':
*
* - 'fields' An array of field names, formatted
* according to cakePHP conventions
* for BelongsTo associations.
*/
class MarshalAssocBehavior extends Behavior {
protected $_defaultConfig = [
'fields' => []
];
public function beforeMarshal (Event $event,
ArrayObject $data,
ArrayObject $options) {
$fields = $this->getConfig('fields');
foreach ($fields as $field) {
$temp = explode('.', $field);
$fd_name = $temp[0];
$column = $temp[1];
unset($temp);
/*
* If @$data does not contain required keys,
* skip and evaluate next config block.
*/
if ( !array_key_exists($fd_name, $data)
|| !array_key_exists($column, $data[$fd_name])
) continue;
$table_name = Inflector::pluralize(Inflector::camelize($fd_name));
$table = TableRegistry::get($table_name);
/**
* @var Cake\Datasource\EntityInterface $result
*/
$result = $table->find()
// value (user-provided) is escaped by Cake
->where([$column => $data[$fd_name][$column]])
->first();
if ($result) {
unset($data[$fd_name]);
$data[$fd_name.'_id'] = $result->id;
}
}
}
}
To incorporate it in my BooksController
:
public function add() {
$this->Books
->addBehavior('MarshalAssoc', [
'fields' => ['philosopher.full_name']);