Archive for the ‘CakePHP’ Category
UploadPack - easy and flexible way to upload files with CakePHP
I have seen and worked with many scripts and plugins (not only written in PHP) that tries to deal with file-upload functionality on server side, thanks to this I think I have some view on how good file-upload plugin should work. The thing was to put it in CakePHP clothes and code it.
Here are the requirements I set:
- save procedure of record with attached file should be no different to usual record
- it should be possible to do some additional processing of uploaded file at the time of saving it (for example thumbnail generation)
- easy access to uploded file, it’s URL and alternatives (different thumnails) from view level
- everything should require no or minimum configuration to work, but still remain flexible if the whole application needs it
- natural integration with CakePHP framework
Here’s the effect of some planning and coding - UploadPack. Right now it contains:
- UploadBehavior - deals with saving files to disk and post-save processing
- UploadHelper - provides nice access to files (URLs, thumbnails) from view level
Everything works quite gracefully, I think. You only need to add one field to model’s database table (which will hold file name) and attach behavior to model. The rest is done automatically. Everything is documented on repository’s page.
It’s still an early version - 0.1, but there is much it can do right now. The work is underway to provide new features. Take a look at it.
If you have any views or ideas, please leave a comment. I’d appreciate it.
NamedScopeBehavior upgrade
I have just added a small functionality to my NamedScopeBehavior.
Now you can use named scopes from multiple models at once in a single find query. Assuming that given models are associated through belongsTo or hasOne association.
Quick example:
// model definitions class User extends AppModel { var $actsAs = array( 'NamedScope' => array( 'activated' => array('User.activated' => 1) ) ); var $belongsTo = array( 'Group' ); } class Group extends AppModel { var $actsAs = array( 'NamedScope' => array( 'admins' => array('Group.name' => 'admins') ) ); } // in controller $this->User->find('all', array('scope' => array('User.activated', 'Group.admins')));
New version can be found in repository.
Named scope in CakePHP
Some time ago I wrote how to improve Cake controllers’ callback system basing on a solution from Ruby on Rails. Today I am going to port another cool feature of Rails to CakePHP - named scope.
Check this Rails example first if you don’t know what the named scope is:
# model definition class User < ActiveRecord::Base # only activated users named_scope :activated, :conditions => "activated_at is not null" # only currently online users named_scope :online, :conditions => "date_add(last_activity, interval 5 minute) < now()" end
This is very simple example. In fact, named scope functionality can be used for much more than this. However my Cake version I present below is limited to defining conditions.
So we defined some scopes and gave them names. Now we can apply them to searching.
# let's find all activated users users = User.activated.find(:all) # let's find all activated users who are currently online and has more than 10 points users = User.activated.online.find(:all, :conditions => "points > 10")
I bet you can already see advantages of such notation. Often used conditions (for example: often you want to limit search results to activated users only) are placed in named_scope definition, the others (probably more situation-specific ones) go to usual :conditions option. Improved readability, less writing, fewer chances to make mistakes.
Let me present you NamedScopeBehavior which will give you similat functionality in CakePHP. What it does is converting scope definitions to conditions parameter of find() method and that is done transparently in beforeFind callback method.
Download named_scope.php file from repository and place it it app/models/behaviors folder of your Cake app. Then attach this behavior to a model and define named scopes.
class User extends AppModel { var $actsAs = array( 'NamedScope' => array( 'activated' => array('User.activated in not null'), 'online' => array('date_add(User.last_activity, interval 5 minute) > now()') ) ); }
Then you can use it.
// in your controller $this->User->find('all', array('scope' => 'activated')); $this->User->find('all', array('conditions' => 'points > 10', 'scope' => array('activated', 'online')))
Pagination also works fine with scopes.
// in your controller $paginate = array( 'User' => array( 'order' => 'created ASC', 'limit' => 20, 'scope' => array('online', 'activated') ) );
Happy developing!
Convenient controller’s callback methods with CakePHP
Controller’s callback methods (beforeFilter, beforeRender and afterFilter) are very useful, they save a lot of time, makes code more readable and DRY. You probably use them a lot. They are great. Unless they’re too big.
The process is simple.
- You want some code executed before every action in your controller, so you put the code in beforeFilter callback.
- Then you want some code executed only before certain actions, so you add the if-then-else block to your before filter that deciced whether to execute the code or not.
- Some code needs to be executed only if the user is logged in, so another if-then-else block is created.
- More conditions creates more mess in your callback…
Could it be changed? Well, look at following snippet which comes from Ruby on Rails.
class UsersController < ApplicationController before_filter :do_something, :do_always before_filter :do_something_else, :only => [:show, :new] before_filter :do_something_different, :except => :edit # method definitions commented out end
What will happen:
- do_something and do_always methods will be called during every request
- do_something_else method will be called only if show or new actions are requested
- do_something_different method will be called during every request except those to edit action
Clean and simple. Every piece of code that needs to be called in before_filter callback (under whatever conditions) is placed in it’s own method. The controller decides what methods to invoke and when.
I prefer this approach to Cake’s built-in one, so I decided to port it.
You need to put this code into your AppController.
class AppController extends Controller { function _callbacks($callbacks) { $defaults = array( 'methods' => array(), 'only' => array(), 'except' => array(), 'if' => array(), 'unless' => array() ); $ifs = $unlesses = $methods = array(); foreach ($callbacks as $array) { $array = am($defaults, $array); foreach ($array as $key => $value) { if (!is_array($value)) { $array[$key] = array($value); } } $ok = true; foreach ($array['if'] as $if) { if (!array_key_exists($if, $ifs)) { $ifs[$if] = $this->dispatchMethod("_$if"); } $ok = $ok && $ifs[$if]; if (!$ok) { break; } } foreach ($array['unless'] as $unless) { if (!array_key_exists($unless, $unlesses)) { $unlesses[$unless] = $this->dispatchMethod("_$unless"); } $ok = $ok && !$unlesses[$unless]; if (!$ok) { break; } } if ($ok) { if (!empty($array['only'])) { if (!in_array($this->action, $array['only'])) { $ok = false; } } elseif (!empty($array['except'])) { if (in_array($this->action, $array['except'])) { $ok = false; } } } if ($ok) { $methods = am($methods, $array['methods']); } } foreach (array_unique($methods) as $method) { $this->dispatchMethod("_$method"); } } function __mergeVars() { $pluginName = Inflector::camelize($this->plugin); $pluginController = $pluginName . 'AppController'; if (is_subclass_of($this, 'AppController') || is_subclass_of($this, $pluginController)) { $appVars = get_class_vars('AppController'); $uses = $appVars['uses']; $merge = array('beforeFilter', 'afterFilter', 'beforeRender'); $plugin = null; if (!empty($this->plugin)) { $plugin = $pluginName . '.'; if (!is_subclass_of($this, $pluginController)) { $pluginController = null; } } else { $pluginController = null; } if ($pluginController) { $pluginVars = get_class_vars($pluginController); } foreach ($merge as $var) { $appVar = (isset($appVars[$var]) && !empty($appVars[$var]) && is_array($appVars[$var])) ? $appVars[$var] : array(); $pluginVar = (isset($pluginVars[$var]) && !empty($pluginVars[$var]) && is_array($pluginVars[$var])) ? $pluginVars[$var] : array(); $thisVar = (isset($this->{$var}) && !empty($this->{$var}) && is_array($this->{$var})) ? $this->{$var} : array(); $this->{$var} = am($appVar, $pluginVar, $thisVar); } } parent::__mergeVars(); } function beforeFilter() { if (!empty($this->beforeFilter)) { $this->_callbacks($this->beforeFilter); } } function afterFilter() { if (!empty($this->afterFilter)) { $this->_callbacks($this->afterFilter); } } function beforeRender() { if (!empty($this->beforeRender)) { $this->_callbacks($this->beforeRender); } } }
Pretty long but worth it. Now you can define in controllers (or AppController, or PluginAppController):
var $beforeFilter = array( array( 'methods' => array('do_something', 'do_always') ), array( 'methods' => array('do_something_else'), 'only' => array('show', 'new') ), array( 'methods' => array('do_something_different'), 'except' => array('edit') ) ); function _do_something() { } function _do_always() { } function _do_something_else() { } function _do_something_different { }
This is equivalent to Rails definition above. Note the underscores before function names. They help distinguish those methods from usual controller actions.
You can do even more:
var $beforeFilter = array( array( 'methods' => array('do_something'), 'if' => array('is_admin') ), array( 'methods' => array('do_something_else'), 'unless' => array('is_logged_in') ) ); function _logged_in() { } function _is_admin() { }
Now do_something method will be called if _is_admin returns true, and do_something_else if _is_logged_in returns false. It’s self-explanatory I think.
Of course you can use all options (only, except, if, unless) together or mix them.
The definitions are processed in order they appear in $beforeFilter (or $beforeRender, or $afterFilter) array.
The definitions from your controller are merged with those defined in PluginAppController and AppController (thanks to redefined __mergeVars() method which is called during controller construction). Firstly AppController’s definitions are processed, then PluginAppController’s, and your controller’s at last.
Enjoy and comment please.
Migration to CakePHP RC2
So far one of my app was running on 1.2.0.6311 version released on 2 Jan 2008. It worked fine, and newer version could break something, so I didn’t bother to upgrade. However now, when new functionalities are to be implemented, I finally decided to migrate (to 1.2.0.7296 RC2). Of course some things stopped working as I expected, so I decided to list them here for others.
- custom validation methods defined in a model must be public now
- App::Import changed again, so my post about switching from vendors() to App::Import from March 2008 is not actual anymore
- structure of core.php file was changed a little, so configuring the file from scratch would be a good idea
- some ‘deprecated’ warnings, easy to correct
Well that is not much, even for such a quite simple application. Honestly, I expected to face more problems. Hopefully they don’t remain undiscovered.
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.
