Archive for the ‘Model’ tag
Tableless models in Rails
If you’re developing an application in Ruby on Rails framework, there are many situations when you should consider using tableless models. The advantages you’d get are:
- consistent application structure, because you’re using models to represent objects in your app
- routes available for free if you also define controllers dedicated for those models, resulting in RESTful application
- easy validation (just like with normal models) and other goodies shipped with Active Record
- easier testing with unit tests
- form building as easy as with normal models
If you’re familiar with “fat model, skinny controller” concept you’ll find more reasons.
An example situation: you give the users an ability to recommends object they find on the site to their friends. They can recommend photos and articles. You do not want to track those recommendations in database. Your model file would look like this.
class Recommendation < ActiveRecord::Base def self.columns() @columns ||= []; end def self.column(name, sql_type = nil, default = nil, null = true) columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null) end column :recommendable_type, :string column :recommendable_id, :integer column :email, :string column :body, :text belongs_to :recommendable, :polymorphic => true validates_presence_of :recommendable validates_associated :recommendable validates_format_of :email, :with => /^$|^\S+\@(\[?)[a-zA-Z0-9\-\.]+\.([a-zA-Z]{2,4}|[0-9]{1,4})(\]?)$/ix validates_presence_of :body end
As you can see the only difference in model definition is that you need to provide the columns yourself.
The idea was borrowed from this snippet.
In the next article I’ll cover polymorphic controllers which will help to convert above example into complete one.
Not so obvious behavior of $uses in CakePHP
In CakePHP you declare what models you’re going to use in a controller by defining $uses variable. You can also define it in AppController to have a certain set of models available in every controller. However be careful when doing it because you may put yourself in an unexpected situation as I did recently.
I need an access to User model in beforeFilter and beforeRender actions on every request so I defined those actions in AppController along with:
$uses = array('User');
When I did this I found out that my SessionsController which handles log in and log out logic stopped working immediately. There is no Session model in the app, SessionController works with User model instead:
$uses = array('User');
The error said: “Database table sessions for model Session was not found”. So it tried to find Session model which doesn’t exist as I stated earlier. But why? I have not told Cake to include anything but User model.
To see what exactly has happened we need to look how Cake determines which models it needs to include and it’s done by merging AppController’s $uses with SessionController’s $uses. Merging is done in usual intuitive way with one exception: when SessionController’s $uses is equal to AppController’s $uses then SessionController’s “native” model (Session model here) is also loaded.
Do you have a rational explanation for such behavior? I have none. It’s very unintuitive for me.
Note that the equality of both controllers’ $uses can happen in two situations:
- both controllers have $uses defined explicitely and they’re equal
- SessionController doesn’t have $uses defined at all and derives it from AppController (parent)
When SessionController is (===) null or false no models are loaded at all, no mather what’s been defined in AppController.
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 :)
Hidden model fields, revisited
Two days ago I wrote a post about hidden model fields. That solution had a drawback - you couldn’t read a field once it was hidden. There was also a bug - hidden fields were shown when their values was null. Here’s upgraded version:
class AppModel extends Model { var $hiddenFields = array(); /** * Sets given fields as hidden. * * @param array $fields List of fields * @return void */ function hideFields($fields = array()) { foreach ($fields as $field) if (!in_array($field, $this->hiddenFields)) $this->hiddenFields[] = $field; } /** * Sets given fields as visible. * * @param array $fields List of fields * @return void */ function showFields($fields = array()) { foreach ($fields as $field) foreach ($this->hiddenFields as $k => $hiddenField) if ($hiddenField == $field) unset($this->hiddenFields[$k]; } /** * Callback function. * Removes fields marked as hidden from the results of find operation. * * @param array $result Results of find operation * @param bool $primary Whether model is being queried directly */ function afterSave($result, $primary = false) { foreach ($result as &$row) foreach ($this->hiddenFields as $field) if (array_key_exists($field, $row[$this->name])) unset($row[$this->name][$field]); } }
The usage is pretty simple and intuitive. You can mark certain fields as hidden in your model definition, as shown in original post, but you can also show and hide them from wherever you like. For example in controller (assuming that ‘password’ field is marked as hidden in User model):
function show_me_my_password() { $this->User->showField(array('password')); $this->User->read(null, 1); // do something... }
It’s just example. Generally you should avoid showing passwords as plain text for security reasons. You should always even keep them in encrypted form, ie. hashed. You won’t need to ever decrypt them. Instead of retrieving password from database for comparision during logging in, hash it and compare hashes. Instead of retrieving it to remind it to the user, generate new one.
Hidden model fields
Probably there are some fields in your models, usually holding sensitive data, that you do not want to display to the end users. In my case these are passwords, activation codes, etc. I want to ensure that values stored in these fields will be never exposed, neither intentionally nor by accident. So I came up with this little solution.
This is implementation of afterFind() callback function that should be placed inside of app_model.php file.
// app_model.php class AppModel extends Model { var $hiddenFields = array(); function afterFind($result, $primary = false) { foreach ($result as &$row) foreach ($this->hiddenFields as $field) if (isset($row[$this->name][$field])) unset($row[$this->name][$field]); return $result; } }
The last thing to do is to define $hiddenFields variable in your model.
var $hiddenFields = array('password', 'activation_code');
As you may have noticed, values of these fields are retrieved from the database and later unset. If you don’t want them to be retrieved at all (this would be more secure), you should take care of filtering this fields out in beforeFind() callback method, however this could be quite cumbersome. For me it was OK enough to just unset them after retrieval, so I even didn’t try to do it through beforeFind().
There are also drawbacks. For example you can’t retrieve someone’s password and send it by e-mail when someone forgets it (in fact you can still get it with model’s query() method, but it’s not a ‘clean’ way). In such situations you should generate new random password instead of reminding the forgotten one.
If you have any views on this, please leave a comment.
