dqyp50298
2017-06-22 18:43
浏览 55
已采纳

CakePHP 3.x:使用文本输入添加BelongsTo实体

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 the Philosopher 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']);
  • 写回答
  • 关注问题
  • 收藏
  • 邀请回答

1条回答 默认 最新

  • dongyou7739 2017-06-22 19:09
    已采纳

    Using beforeMarshal to modify the data accordingly is the way to go, however you'd need to populate the foreign key in the books data instead, ie set philosopher_id, and remove philosopher:

    unset($data['philosopher']);
    $data['philosopher_id'] = $result->id;
    

    philosopher.id would only be used when updating an existing record.

    Also TableRegistry::exists() might bail out even if you wouldn't expect it to, as there might have no instance been set yet!

    And last but not least, where($data[$field]) is dangerous, it's a possible SQL injection vulnerability, as the key hand side of the passed array will be inserted into the query as is (the whole value could also be a string, which would be inserted as is too), and can potentially be defined by the user. If you want to use such configurable/reusable/dynamic functionality, then you should implement a whitelist for the fieldnames, and build the conditions array yourself.

    已采纳该答案
    打赏 评论

相关推荐 更多相似问题