Read-only model fields

How many times you found yourself in situation when you were trying to prevent certain fields from being edited by users? I’ve done it myself and I’ve seen others doing it. The most hardcore method is to unset() long list of fields in $this->data variable in controller’s action. It can get difficult to maintain if you change fields’ values from many places – if you forget to unset a sensitive field somewhere this could happen in serious security flaw in your application.

Imagine that your user model has such fields: id, username, password, group_id. Of course you let users change their username and password, but it’s critical to keep them away from group_id field that assigns them to certain user group to make sure they don’t make themselves Administrators!

My proposal is to control these unwanted changes from model level. Let’s call these read-only fields ‘locked fields’. We are going to define which field to lock in our model’s definition and the beforeSave() callback method will take care of the rest. Here is the AppModel:

class AppModel extends Model {
  var $lockedFields = array();
 
  function beforeSave() {
    if (!empty($this->id)) {
      foreach ($this->lockedFields as $field) {
        unset($this->data[$this->name][$field]);
      }
    }
    return true;
  }
 
  function lockFields($fields = array()) {
    if (!is_array($fields)) {
      $fields = array($fields);
    }
    foreach ($fields as $field) {
      if (!in_array($field, $this->lockedFields)) {
        $this->lockedFields[] = $field;
      }
    }
  }
 
  function unlockFields($fields = array()) {
    if (!is_array($fields)) {
      $fields = array($fields);
    }
    foreach ($this->lockedFields as $k => $field) {
      if (in_array($field, $fields)) {
        unset($this->lockedFields[$k]);
      }
    }
  }
}

As you see beforeSave() does it’s job only when a record is being edited ($this->id is set) as it should. UserModel looks like this:

// user.php
class UserModel extends AppModel {
  var $lockedFields = array('group_id');
}

We defined group_id as locked field in model’s definition but thanks to lockFields() and unlockFields() methods defined in AppModel we are able to alter this dinamically. There may be occasion when administrator actually wants to move an user to another group. You can achieve it through a simple method in UserModel.

// user.php
function moveToAdmins($userId) {
  $this->unlockFields('group_id');
  $this->id = $userId;
  $this->saveField('group_id', 1);
  $this->lockFields('group_id');
}

Simple. You also can lock/unlock many fields at once passing them in array.

As always I hope this helps someone :)

  • http://rafaelbandeira3.wordpress.com rafaelbandeira3

    It’s a nice feature, just not sure if it’s the best implementation.

    nice code, nice blog

  • Michał Szajbe

    I agree, it’s an old and already outdated post.

    Similar functionality has been added to the core.

    Check the $whitelist variable in model. You can assign it an array containing list of field that can be edited (other fields will be ignored when saving model).

    You can also pass such an array as $fields variable when calling $model->save().

    The definition of save method is in fact:
    function save($data = null, $validate = true, $fields) { … }
    This $fields behaves like $whitelist, but only for that one save call.