Sampo Sarrala
Sampo Sarrala

Reputation: 4886

CakePHP 3 Saving HasOne association

possible bug in cakephp framework but not sure about that

I got following MySQL InnoDB tables:

database.users
    +-----+---------------+----------+
    | id  | user_group_id | username |
    +-----+---------------+----------+
    | INT |           INT | VARCHAR  |

database.user_settings
    +-----+---------------+----------+
    | id  |       user_id |     data |
    +-----+---------------+----------+
    | INT |           INT |  VARCHAR |

I got following intialization in table classes:

  • Model\Table\UsersTable:

    $this->table('users');
    $this->displayField('id');
    $this->primaryKey('id');
    $this->belongsTo('UserGroups', [
        'foreignKey' => 'user_group_id'
    ]);
    $this->hasOne('UserSettings', [
        'foreignKey' => 'user_id'
    ]);
    

  • Model\Table\UserSettingsTable

    $this->table('user_settings');
    $this->displayField('id');
    $this->primaryKey('id');
    $this->belongsTo('Users', [
        'foreignKey' => 'user_id'
    ]);
    
    // And folowing validation rules:
    $validator
        ->add('id', 'valid', ['rule' => 'numeric'])
        ->allowEmpty('id', 'create')
        // UserSettings.user_id validation rule:
        ->add('user_id', 'valid', ['rule' => 'numeric'])
        ->requirePresence('user_id', 'create')
        ->notEmpty('user_id');
    

    And I got following code:

    $user = $this->Users->newEntity();
    if ($this->request->is('post')) {
        $user = $this->Users->patchEntity($user, $this->request->data, [
            'associated' => ['Users.UserSettings']
        ]);
        // Tried it also this way, won't change anything
        //$user = $this->Users->patchEntity($user, $this->request->data, [
        //  'associated' => ['user_setting']
        //]);
        $this->Users->save($user,['associated' => ['UserSettings']]);
    }
    

    Sample input data ($this->request->data):

    [
        'user_group_id' => 1, // Not related to question
        'username' => 'test', // This will be saved without core modifications
        'user_setting' => [
            'data' => 'sample data' // Saved only after "fix" described below
        ]
    ];
    

    This will save parent table (users) but not child table (user_settings).

    Then I got following modification I want to get rid of:

    If I put this change to ORM\Associations\HasOne::saveAssociated(...)

    // $this->property() == 'user_setting'
    if (is_array($targetEntity)) {
        $targetEntity = $this->target()->newEntity($targetEntity);
    }
    

    It will instantly work the way I wanted to. HasOne actually has required data but it also checks if data is inside Entity, in this case it was in array.

    I've played with different association naming combinations and it seems that this is how it should be.

    Main question is how should I save optional hasOne association with parent row?

    Can I somehow make sure that data will be converted into Entity object? I think it should just work as there is all required data available and it also seems to handle relations correctly.

    Upvotes: 2

    Views: 3138

  • Answers (2)

    Sampo Sarrala
    Sampo Sarrala

    Reputation: 4886

    With help provided by @ndm I managed to solve this problem with custom marshaller.

    I first did take a look on separate validators but for me it seems like too complicated and fragile way to solve very simple straightforward problem.

    My honest opinion is that all this should actually be taken care inside framework core somehow.

    Solving required fk with custom marshaller:

    This way it should be sure that "custom validation" will be used only when UserSettings will be created through Users and user_settings.user_id should be readily available.

    class UsersMarshaller extends Marshaller {
        protected function _validate($data, $options, $isNew) {
            $errors = parent::_validate($data, $options, $isNew);
            if ($isNew) {
                unset($errors['user_setting']['user_id']['_required']);
            }
            return $errors;
        }
    }
    

    And in UsersTable class:

    public function marshaller() {
        return new UsersMarshaller($this);
    }
    

    Actual errors in original question:

    patchEntity(...) call parameters was faulty even while those parameters did something that made them look like they might be correct. Associations should be this way: ['associated' => ['UserSettings']]. Not Users.UserSettings or user_setting.

    Upvotes: 2

    ndm
    ndm

    Reputation: 60503

    The correct value for the associated property would be UserSettings, what you have used, Users.UserSettings, would be a Users > Users > UserSettings association.

    Upvotes: 1

    Related Questions