Just a quick update – I’m currently working on integrating some libraries from the Zend Framework into CakePHP. The first one I’m doing is Zend_Validate, which is a heavy, robust alternative to CakePHP’s own Validation class. I’ll be sharing my results as a plugin when I’m done. Stay tuned.
The Validation::postal() method that comes with CakePHP 1.2 is good in that it can handle a number of different country formats, but the problem is you can only validate your data against one country. What if you want to accept, say, either Canadian or US postal/zip code formats? I ran into this problem earlier today, and decided to write my own postal() function that can take either a string as the country, just like Validation::postal(), or an array of countries.
Here’s the function, which also resides in its Github repository:
<?php
class AppModel extends Model {
/**
* Modified version of Validation::postal - allows for multiple
* countries to be specified as an array.
*/
function postal($check, $regex = null, $country = null) {
// List of regular expressions to use, if a custom one isn't specified.
$countryRegs = array(
'uk' => '/\\A\\b[A-Z]{1,2}[0-9][A-Z0-9]? [0-9][ABD-HJLNP-UW-Z]{2}\\b\\z/i',
'ca' => '/\\A\\b[ABCEGHJKLMNPRSTVXY][0-9][A-Z][ ]?[0-9][A-Z][0-9]\\b\\z/i',
'it' => '/^[0-9]{5}$/i',
'de' => '/^[0-9]{5}$/i',
'be' => '/^[1-9]{1}[0-9]{3}$/i',
'us' => '/\\A\\b[0-9]{5}(?:-[0-9]{4})?\\b\\z/i',
'default' => '/\\A\\b[0-9]{5}(?:-[0-9]{4})?\\b\\z/i' // Same as US.
);
$value = array_values($check);
$value = $value[0];
if ($regex) {
return preg_match($regex, $value);
} else if (!is_array($country)) {
return preg_match($countryRegs[$country], $value);
}
foreach ($country as $check) {
if (!isset($countryRegs[$check]) && preg_match($countryRegs['default'], $value)) {
return true;
} else if (preg_match($countryRegs[$check], $value)) {
return true;
}
}
return false;
}
}
?>
I put the function in my AppModel, but you can put it in an individual model if you don’t want it to apply to every model in your application. Usage is pretty simple. For example, to validate data that can be either US or Canadian format:
<?php
class MyModel extends AppModel {
var $validate = array(
'postal_zip' => array(
'rule' => array('postal', null, array('us', 'ca')),
'message' => 'Please enter a valid postal or zip code.',
'required' => true
)
);
}
?>
How often does an impromptu outdoor ice rink in an abandoned lot materialize in balmy Victoria? We couldn’t pass up the opportunity. So, a few of us from the office and a couple of hockey buddies laced up during lunch:
There are two main methods for finding random records with PHP (and in this case CakePHP) and MySQL:
While option #2 might seem to be slower – two queries and intermediary PHP work! – but ORDER BY RAND() tends to churn to an almost-grinding halt when queries tables with lots of records. So, #2 it is. And, since we’re working with CakePHP, we can abstract it to the Model::find() level with a custom __findXX method that I like to call __findRandom(). Note that usage of this function relies on Matt Curry’s implementation of custom find methods (I seem to link to that bit of code in every post!).
The basic idea is that two queries are executed: first Model::find->(‘list’) generate a list of primary keys, and then either Model::find->(‘first’) or Model::find(‘all’), depending on whether we’re looking for one or more records.
The code is available in my GitHub repository.
Usage is as simple as (using the User model as an example):
$record = $this->User->find('random');
That’ll return one random record from the User model. Like most Model::find functions, you can pass an array as an optional second argument:
$records = $this->User->find('random', array(
'amount' => 5,
'list' => array(
'conditions' => array('User.active' => 1)
),
'find' => array(
'contain' => array('Group')
)
));
As you can see, you’re actually working with two nested arrays for your query modifiers, ‘list’ and ‘find’. Use these arrays to modify the the find(‘list’) and find(‘first’)/find(‘all’) queries respectively. Use the ‘amount’ option, which defaults to 1, to specify the maximum records you want returned.
You can also bypass the find(‘list’) query entirely by passing an array of primary keys as the ‘suppliedList’ argument. For example:
$records = $this->User->find('random', array(
'amount' => 5,
'suppliedList' => $myIDs,
'find' => array(
'contain' => array('Group')
)
));
As you can see, it’s a fairly simple function. But, it gets the job done! Here’s the source code, which, as mentioned above, you can grab on GitHub as well:
<?php
class AppModel extends Model {
function find($type, $options = array()) {
$method = null;
if (is_string($type)) {
$method = sprintf('__find%s', Inflector::camelize($type));
}
if ($method && method_exists($this, $method)) {
$results = $this->{$method}($options);
} else {
$args = func_get_args();
$results = call_user_func_array(array('parent', 'find'), $args);
}
return $results;
}
/**
* __findRandom()
*
* Find a list of records ordered by rank.
* Instead of executing a __findList() query to get the list of IDs,
* you can pass an array of IDs via the $options['suppliedList']
* argument.
*
* Two queries are executed, first a find('list') to generate a list of primary
* keys, and then either a find('all') or find('first') depending on the return
* amount specified (default 1).
*
* Pass find options to each query using the $options['list'] and $options['find']
* arguments.
*
* Specify $options['amount'] as the maximum number of random items that should
* be returned.
*
* If you already have an array of IDs(/primary keys), you can skip the find('list')
* query by passing the array as $options['suppliedList'].
*
* @access private
* @param $options array of standard and function-specific find options.
* @return array
*/
function __findRandom($options = array()) {
if (!isset($options['amount'])) {
$amount = 1;
} else {
$amount = $options['amount'];
}
$findOptions = array();
if (isset($options['find'])) {
$findOptions = array_merge($findOptions, $options['find']);
}
if (!isset($options['suppliedList'])) {
$listOptions = array();
if (isset($options['list'])) {
$listOptions = array_merge($listOptions, $options['list']);
}
$list = $this->find('list', $listOptions);
} else {
$list = $options['suppliedList'];
$list = array_flip($list);
}
// Just a little failsafe.
if (count($list) < 1) {
return $list;
}
$originalAmount = null;
if ($amount > count($list)) {
$originalAmount = $amount;
$amount = count($list);
}
$id = array_rand($list, $amount);
if (is_array($id)) {
shuffle($id);
}
if (!isset($findOptions['conditions'])) {
$findOptions['conditions'] = array();
}
$findOptions['conditions'][$this->alias.'.'.$this->primaryKey] = $id;
if ($amount == 1 && !$originalAmount) {
return $this->find('first', $findOptions);
} else {
return $this->find('all', $findOptions);
}
}
}
?>
If you use CakePHP’s caching system but don’t like having to wrap you calls to Model::find() in if statements (“if cache result found… else…”) then this little quick tip is for you. Basically, we’re just going to put a slightly modified version of Model::find() in AppModel. Our new find() method will check for the existence of a ‘cache’ argument in the options array. Then, the function will either grab the query results from the cache or run a DB query, depending on what’s passed in the ‘cache’ argument.
A couple of notes:
We’re putting two methods into AppModel: find() and __getCachedResults(), a helper function. You can download the latest version of this code at my GitHub repository, or grab it from the bottom of this post.
Usage is simple:
The code:
<?php
class AppModel extends Model {
/**
* Adds support for custom find methods (__findXX) and automatic caching.
* Automatic caching will kick in when 'cache' is passed in the $options
* array.
*
* If 'cache' is a string, then it will be used to generate the
* cache name, which takes the format model_alias_cache_name.
*
* If 'cache' is an array, then two arguments are valid: 'name', required,
* and 'config', optional. 'name' is used as above, while 'config'
* determines the cache configuration to use - 'default' if not specified.
*/
function find($type, $options = array()) {
$results = $this->_getCachedResults($options);
if (!$results) {
$method = null;
if (is_string($type)) {
$method = sprintf('__find%s', Inflector::camelize($type));
}
if ($method && method_exists($this, $method)) {
$results = $this->{$method}($options);
} else {
$args = func_get_args();
$results = call_user_func_array(array('parent', 'find'), $args);
}
if ($this->useCache) {
Cache::write($this->cacheName, $results, $this->cacheConfig);
}
}
return $results;
}
function _getCachedResults($options) {
$this->useCache = true;
if (Configure::read('debug') > 0 || !is_array($options) || !isset($options['cache']) || $options['cache'] == false) {
$this->useCache = false;
return false;
}
if (is_string($options['cache'])) {
$this->cacheName = $this->alias . '_' . $options['cache'];
} else {
if (!isset($options['cache']['name'])) {
return false;
}
$this->cacheName = $this->alias . '_' . $options['cache']['name'];
$this->cacheConfig = isset($options['cache']['config']) ? $options['cache']['config'] : 'default';
}
$results = Cache::read($this->cacheName, $this->cacheConfig);
return $results;
}
}
?>
Yup, I’ve finally joined the party and created a GitHub account for myself. There’s not much on there – currently just one bit of code, a CakePHP component. I’m hopefully going to fill out the repository with a bunch of uself stuff soon.
Most people who do serious, large scale development with CakePHP would agree that keeping the bulk of a project’s code segregated in plugins is the best way to keep things organized. Sometimes, however, we want to use a plugin’s functionality outside of the scope of that plugin’s controllers. For example, we might have a products plugin with a ‘feature product’ capability, which we might want to display on our site’s homepage. So what are the options? We could just stick some code in the AppController (which I covered in a previous post – my old Widget Component), or you could use the requestAction() function to call a method from another controller. But, both of those options have their faults: putting all of that code in the AppController can quickly add up, while requestAction() eats up a lot of resources since it initiates an entirely new request.
So, we need a solution that keeps the plugin code in the plugin directory and doesn’t use a ton of resources. And that’s where plugin controller callbacks come in. The Cake core really should have built-in callback functionality for plugin controllers, so developers can, for example, set a certain plugin action to fire after every beforeFilter(), during every beforeRender(), etc. But until it’s a part of the core, the community needs to fend for itself. There are a few implementations of plugin callbacks/hooks floating around in the community, but none of them suited my purposes exactly. So, I figured what the hell, I’ll try doing my own! And just like that, my RegisterCallbacksComponent was born.
I used a bakery article called “PluginHandler to load configuration and callbacks for plugins” by Gediminas Morkevicius (sky_l3ppard) as a starting point, since I liked the approach he used. I boiled it down, however, to make it simpler and just for callbacks (he handles other auto-loading aspects as well, such as routes).
The component is at the bottom of the post, after the instructions. You can also download it from its repository in my brand-spankin’-new Github account.
The basic idea is that each plugin has a “Callback” class – CamelizedPluginNameCallback (for example, PhotoGalleriesCallback if you’re working with a PhotoGalleries plugin). Within that class are your callback functions which are executed by the component.
Now, I’ve only done some cursory testing at this point, but the component is pretty straightforward and everything seems to be working. To use:
var $components = array('RegisterCallbacks');var $components = array('RegisterCallbacks' => array('priority' => array('ImportantPluginOne, 'ImportantPluginTwo')));Configure::listobjects('plugin')And that’s basically it. Now you can keep your application-wide plugin code where it belongs – with the plugins. Here’s the full component code, current as of the time of writing. For up-to-date code, use my Github repository.
<?php
/**
* RegisterCallbacksComponent class file
*
* Executes callback functions for each plugin before each controller callback action.
* Files should be located in: /plugins/[plugin_name]/[plugin_name]_callback.php
*
* Inspiration, architecture, and some code from:
* http://bakery.cakephp.org/articles/view/pluginhandler-to-load-configuration-and-callbacks-for-plugins
* 'PluginHandler to load configuration and callbacks for plugin' - Gediminas Morkevicius (sky_l3ppard)
*
* @filesource
* @author Jamie Nay
* @copyright Jamie Nay
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @link http://jamienay.com/2009/11/an-easy-plugin-callback-component-for-cakephp-12/
*/
class RegisterCallbacksComponent extends Object {
/**
* Controller object
*
* @var object
* @access private
*/
var $__controller = null;
/**
* Component settings
*
* @access public
* @var array
*/
public $settings = array();
/**
* Default values for settings.
* - priority (optional): the order in which callbacks should be executed. If
* priority is left empty, or if some plugins are left out of the list, the
* plugins are just added in the order in which they're loaded via Configure.
*
* @access private
* @var array
*/
private $__defaults = array(
'priority' => array()
);
/**
* Registered plugins - plugins that have a PluginNameCallback class.
*
* @access private
* @var array
*/
private $__registered = array();
/**
* Configuration method.
*
* In addition to configuring the settings (see $__defaults above for settings explanation),
* this function also loops through the installed plugins and 'registers' those that have a
* PluginNameCallback class.
*
* @param object $controller Controller object
* @param array $settings Component settings
* @access public
* @return void
*/
public function initialize(&amp;$controller, $settings = array()) {
$this->__controller = &amp;$controller;
$this->settings = array_merge($this->__defaults, $settings);
if (empty($this->settings['priority'])) {
$this->settings['priority'] = Configure::listobjects('plugin');
} else {
foreach (Configure::listobjects('plugin') as $plugin) {
if (!in_array($plugin, $this->settings['priority'])) {
array_push($this->settings['priority'], $plugin);
}
}
}
foreach ($this->settings['priority'] as $plugin) {
$file = Inflector::underscore($plugin).'_callback';
$className = $plugin.'Callback';
if (App::import('File', $className, true, array(APP . 'plugins' . DS . Inflector::underscore($plugin)), $file.'.php')) {
if (class_exists($className)) {
$class = new $className();
ClassRegistry::addObject($className, $class);
$this->__registered[] = $className;
}
}
}
/**
* Called before the controller's beforeFilter method.
*/
$this->executeCallbacks('initialize');
}
/**
* Executes beforeFilter() methods in the callback classes.
* Called after the controller's beforeFilter() method but before
* the controller executes the current action handler.
* Uses 'beforeFilter' instead of 'startup' to make the action name
* more consistent with the controller name.
*
* @param object $controller Controller object
* @access public
* @return void
*/
public function startup(&amp;$controller) {
$this->executeCallbacks('beforeFilter');
}
/**
* Executes beforeRender() methods in the callback classes.
* Called after the controller's beforeRender method but before the
* controller renders views and layout.
*
* @param object $controller Controller object
* @access public
* @return void
*/
public function beforeRender(&amp;$controller) {
$this->executeCallbacks('beforeRender');
}
/**
* Executes shutdown() methods in the callback classes.
* Called before output is sent to the browser.
*
* @param object $controller Controller object
* @access public
* @return void
*/
public function shutdown(&amp;$controller) {
$this->executeCallbacks('shutdown');
}
/**
* Executes beforeRedirect() methods in the callback classes.
* Called when the controller's redirect method is called but
* before any further action.
*
* @param object $controller Controller object
* @param string $url
* @param string $status
* @param boolean $exit
* @access public
* @return void
*/
public function beforeRedirect(&amp;$controller, $url, $status = null, $exit = true) {
$this->executeCallbacks('beforeRedirect', array($url, $status, $exit));
}
/**
* Executes the requested method in each Callback class, in order
* of priority, if that method exists in the class. Also sends any
* arguments, with the $this->__controller always being the first
* argument.
*
* @param string $method Method name
* @param array $args Optional arguments
* @access public
* @return void
*/
public function executeCallbacks($method, $args = array()) {
foreach ($this->__registered as $callback) {
$class = ClassRegistry::init($callback);
if ($class &amp;&amp; in_array($method, get_class_methods($class))) {
call_user_func_array(array($class, $method), array_merge(array($this->__controller), $args));
}
}
}
}
?>
Wow! It’s been a while since I’ve written much about CakePHP, PHP, or just anything useful in general. I guess I’ve been pretty busy. Hectic job, baby on the way, that sort of thing. But I’m trying to get back into blogging because it’s a good outlet for my desire to write, plus I love to share useful code with others.
I kept up this useful tutorial roundup for a little while and then dropped off the face of the earth… oops. So, let’s try this again – the Friday tutorial roundup. Even though I haven’t been blogging about it, I’m constantly coming across useful CakePHP tips, code, and tutorials that save me a lot of time. So I thought it might be useful to others to gather up some tutorials every Friday and just get them out there. Some are new; some are old chestnuts.
Here’s what I’m finding useful these days:
I’m officially fed up with my latest WordPress theme. So, I’m going to try out a couple to try to find the best fit. Yes, I’m a web developer and yes, I should come up with my own, but…
Just a quick bit of advice to those who may have been banging their heads against the walls when trying to pass “form” as a custom parameter in a CakePHP URL. Apparently ‘form’ is a param – an array, to be specific – that’s already set by Cake and using it as a custom parameter won’t work. While finishing up an online email form builder plugin, I defined this route:
Router::connect('/:form/', array('plugin' => 'email_forms', 'controller' => 'email_forms', 'action' => 'view'), array('form' => '[a-z0-9_-]+'));
This didn’t work, however; the route would take me to the correct action, but the ‘form’ param would always be empty – or so I thought. After doing some checking, mostly thanks to the Debug Kit plugin, I realized that the ‘form’ param is passed with every request. So, I just renamed ‘form’ to ‘emailForm’ and it worked like a charm:
Router::connect( '/:emailForm/', array('plugin' => 'email_forms', 'controller' => 'email_forms', 'action' => 'view'), array('emailForm' => '[a-z0-9_-]+'));
So, watch out for those predefined variables.