Archive for the ‘CakePHP’ tag
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.
Switching to App::import() may cause problems
Some time ago importing 3rd party libraries through vendor() method became deprecated and now usage of App::import(’Vendor’, ‘…’) is suggested. I decided to walk through my application files and switch to new version. It was not that easy however… In my app I use Swift Mailer library for sending e-mails.
// I changed this... vendor('Swiftlib/Swift/Connection/SMTP'); // to this... App::import('Vendor', 'Swiftlib/Swift/Connection/SMTP');
It didn’t work, because Cake’s Inflector translated the path to swiftlib/swift/connection/s_m_t_p.php. Of course such file does not exist. Changing vendor’s file names wouldn’t be a good idea. After checking the API I found that App::import() also takes $file parameter if you want to explicitly specify which file to include. So the working solution looks like this:
App::import('Vendor', null, true, array(), APP.'vendors/Swiftlib/Swift/Connection/SMTP.php');
Remember that when you use 3rd party libraries and they do not follow Cake’s conventions in naming files.
CakePHP - Some good practices
When you write small applications, you don’t really need to care about quality of your code. However this is a must in medium or large ones. If you don’t spend some time on planning, there is a reasonable chance that you’ll end up with an unfinished project. After writing some in Cake, I have some views on how to write good (or not bad, at least!). Big part of what you’ll read below is related to software engineering in general.
1. Plan the structure of your application
This is the most important. Analyze requirements, identify objects. It’s good to divide application to separable modules and implement and test them one at the time. Connect them after they’re tested. Advantages of such approach are obvious - high cohesion, easier implementation and testing, maintainability. It’s also easy to assign tasks to programmers, because they can work independently.
2. Implement functionalities with abstraction in mind
Try to write each module of the application in a way that it could be reused in another projects without much changes (preferrably only configuration ones). For example let’s consider following situation: you want users of your site to be able to rate objects (photos, comments, whatever…).
database table ratings:
- id - primary key
- user_id - id of user that rates an object
- model - name of the model of an object (Photo, Comment, etc)
- foreign_key - id of an object
- rating - actual rating chosed by an user
Such structure lets users rate any object in the system. I’d suggest writing a Behavior that shares a method to write ratings in database. Then to make an object ratable you’d only need to add this behavior to $actsAs property of object’s model. Simple and generic.
3. Keep controllers independent of user interface
Controller’s action should be implemented in a way, that there is no difference whether they’re accessed via AJAX or usual request. It’s views (and layouts) that are responsible for user interface. If you want to create rich AJAX application, alter the views. Controllers and their action, if correctly written, may remain unchanged.
4. Move the heart of your application to models
You should place most of business logic of your app in models as the concept “Fat model, skinny controller” suggests. Often many controllers share models beetwen them and perform the same actions on them. Placing these actions inside models makes it easier to test, debug and change them, because all the changes need to be made in one place only. Similarly, implementation of such mechanisms as caching or logging is much easier to do and maintain. It’s less error-prone.
5. Use elements extensively
Many pages of the site display the same elements - nav bars, breadcrumbs, menus, etc. Define them in elements. You can pass variables with renderElement() method so you can customize them anytime. For example: create and edit form are almost the same, the only difference is probably the value of action attribute of form tag. So it’s good to make them an element.
Remember that it’s the first controllers and models you write that determine your application’s structure and the way it develops. In the middle of the work it’s hard to change the approach, so you should invest some time in analyzing and planning before you start coding. It will pay off.
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 :)
Ajax login redirection
Users often encounter situations when they want to perform an action on a web site and get redirected to login page. Application’s task is to take care of performing the requested action right after user’s successful log in. It is easy job, unless the action is requested via ajax.
Below I present a simple solution to this problem. Let me stress the word ’simple’, because I’m not 100% satisfied with this. It works fine, however, and I didn’t have time to think about something prettier.
Here’s users_controller.php file which defines login and ajax_login actions. The former one is supposed to take care of logging the user in the system, the latter only redirects to login page via ajax response.
// users_controller.php class UsersController extends AppController { var $name = 'Users'; function login() { //… check login credentials // let's assume they were ok, then perform the action requested by the user before logging in if ($this->Session->check('loginRedirectUrl')) { if ($this->Session->check('loginRedirectParams')) { $params = $this->Session->read('loginRedirectParams'); $params[] = 'return'; $this->Session->del('loginRedirectParams'); $this->requestAction($params['url']['url'], $params); } $url = $this->Session->read('loginRedirectUrl'); $this->Session->del('loginRedirectUrl'); $this->redirect($url); } } function ajax_login() { $this->render('ajax_login', 'ajax'); } /** * action that is accessed through ajax request */ function ajax_action() { $this->checkSession(); // do something creative } }
Now create view file: ajax_login.ctp. It will contain JavaScript code that will make the browser load login page. That is something I don’t feel fully comfortable with, because you must handle ajax response in your view files to place this code in page body from where the browser can read it and execute. Since most ajax requests handles their responses, this will work fine, but probably there are some that don’t. You should try something else on such occasions.
// ajax_login.ctp <script type="text/javascript"> window.location = '<?php echo 'http://'.$_SERVER['HTTP_HOST'].$html->url('users/login').'"'; ?>'; </script>
The last piece of equation is app_controller.php file where we place a method that checks wheter the user is logged in and redirects him to login page if he’s not. Call this method at the beginning of every action/method that is restricted to registered users only.
// app_controller.php class AppController extends Controller { var $components = array('Session', 'RequestHandler'); function checkSession() { if (!$this->Session->check('User')) { // when the user is not logged in if ($this->RequestHandler->isAjax()) { $this->Session->write('loginRedirectUrl', $this->referer()); $this->Session->write('loginRedirectParams', $this->params); $this->requestAction('/users/ajax_login'); } else { $this->Session->write('loginRedirectUrl', $this->params['url']['url']); $this->redirect('/users/login'); } } } }
That’s it. It works both for ajax and usual requests, CakePHP 1.1 and 1.2. I just can’t figure out how to avoid this JS trick, if you have any ideas please let me know.
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.
