Archive for the ‘CakePHP’ Category
CakePHP moved to Github!
After two of the core developers, Nate Abele and Gwoo, left the project lately, many feared the worst. Now it’s a different story! The CakePHP core team decided to move the project to Github.
I’d say: finally!
Now it’s truly open to the community. That’s because Github makes it so easy to collaborate on open source projects. All you need to do is to fork a project, merge your changes, and send a pull request to project’s maintainer. Can’t imagine more painless process.
What I expect now is a real boost in CakePHP development, with many great features delivered by the community. Just check how other projects benefited from the move, for example Ruby on Rails that has more than 700 forks now. So…
Let’s fork it! ;)
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.
