Useful CakePHP Tutorial Roundup for January 29, 2010

Posted in CakePHP on January 29th, 2010 by Jamie – 1 Comment

Well, I’ve finally gathered enough good CakePHP links to warrant another tutorial roundup. It’s not that there haven’t been any good CakePHP posts out there – the blogs are full of ‘em, especially with 1.3 finally in beta – it’s just that I’ve been (and still am) pretty busy. But as always, 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 once in a while and just get them out there. Some are new; some are old chestnuts.

Three quality community resources to showcase today:

  • MultiTree BehaviorThom (cyberthom) (Jan 27, 2010)
    Thom provides a robust, functional alternative to Cake’s core tree behavior for those who want multiple trees in one table. I wrote a little hack of the Tree behavior to accomplish this same task, but the MultiTree Behavior is a hell of a lot better. Nice job, Thom! The methods have mostly the same function names as the core Tree behavior, so you can basically drop this behavior in and then just change your $actsAs “Tree” to $actsAs “MultiTree”. I started using it this morning so I haven’t put it through all of its paces, but it seems pretty solid.
  • Providing common functionality with AppShell – Joe Beeson (Oct 26, 2009)
    This little tutorial/accompanying script implements an “AppShell” that you can use a parent for your Shell classes, in the spirit of AppModel and AppController. I’ll be honest, I haven’t actually used Joe’s code yet, but the idea is great. I’m a bit surprised that the core doesn’t already provide this functionality – instead of YourShell -> AppShell -> Shell (just like YourModel -> AppModel -> Model), the extension chain is just YourShell -> Shell. I guess that AppShell isn’t in the core simply because the Shell class is woefully underused by most developers. But if you find yourself writing a lot of shell scripts with common functionality, then AppShell looks like a good time saver.
  • Authsome Component – Felix Geisendörfer, Debuggable (Dec 25, 2009)
    This is another “haven’t used it yet, but definitely will” bit of code by Felix Geisendörfer, who’s released his share of nice code. Authsome is a less intrusive replacement for the core Auth component, and it looks pretty slick and simple. It handles logging in a lot better than the Auth component, and I like the static Authsome::get(’user_variable’) functionality.

Major Update / Make-Workage to Zend_Search_Lucene Datasource for CakePHP

Posted in CakePHP on January 22nd, 2010 by Jamie – Be the first to comment

I’ve majorly retooled my recently released Zend_Search_Lucene datasource for CakePHP. You can find the latest version on Github, and I’ve also updated the tutorial to reflect the changes.

htaccess trick – redirect to WWW domain without hardcoding the domain name

Posted in Server on January 22nd, 2010 by Jamie – Be the first to comment

I’m notoriously bad at writing good htaccess redirection rules, so when it came time to write a rule that would redirect any non-www URL to its www equivalent, I was a bit lost. I wanted a rule that I could apply to any website without needing to hard-code the domain. I found a few examples after some Google searching, but nothing worked for me. So, a colleague and I came up with this rule, which does the job:

RewriteCond %{HTTP_HOST} ^([a-z-]+)\.([a-z]{2,6})$ [NC]
RewriteRule ^(.*)$ http://www.%1\.%2/$1 [R=301,L]

Can it be improved? You tell me!

Zend_Search_Lucene Datasource for CakePHP

Posted in CakePHP, Zend Framework on January 13th, 2010 by Jamie – 8 Comments

Major update January 22/10: much of the content of this article has been updated to reflect the changes to the datasource, the latest version of which you can download on Github.

Just out of the oven – a Zend_Search_Lucene datasource for CakePHP (built with 1.2 but probably works just fine in 1.3) that I originally wrote for an in-house CMS site search plugin. I can’t release the plugin itself (and there’s so much CMS-specific code that it would need a lot of work to make it generic anyway), but I thought that someone might find the datasource itself useful. It’s pretty basic at this point and doesn’t implement some of the fancier Zend_Search_Lucene features such as sorting (it just returns sorted in score order, which is probably what you want anyway).

Zend_Search_Lucene is a text-based search index system for developers who don’t want to (or can’t) use a database for search indexing.

Download the current version of the ZendSearchLuceneDatsource from my Github repository.

I won’t go into detail about how to add data into the Lucene database since the Zend Framework documention is so good (CakePHP should be jealous!). You’ll find all the info you need there. There are also a couple of older articles out there that show how you can integrate Zend_Search_Lucene into CakePHP:

Setup

First, copy zend_search_lucene.php to models/datasources.

Then, you’ll need to download the Zend_Search_Lucene library from the Zend Framework website and put some files into your /vendors directory:

  • Zend/Search (the directory and all of its contents)
  • Zend/Exception.php

You’ll also need to update your include path to include app/vendors, since the Zend Framework loads a lot of classes on its own. I also made a little autoload function to make the loading of Zend Framework classes easier. Put the following code somewhere common, such as app/bootstrap.php:

ini_set('include_path', ini_get('include_path') . ':' . CAKE_CORE_INCLUDE_PATH . DS . '/vendors');
function __autoload($path) {
if (substr($path, 0, 5) == 'Zend_') {
include str_replace('_', '/', $path) . '.php';
}
return $path;
}

You also need to put the DB config for the datasource in config/database.php (updated Jan 20/2010 for better DebugKit compatibility):

var $zendSearchLucene = array(
	'datasource' => 'ZendSearchLucene',
	'indexFile' => 'lucene', // stored in the cache dir.
	'driver' => '',
	'source' => 'search_indices'
);

Then, in the model that’ll act as your search index (say, for example, SearchIndex), specify the DB config:

<?php class SearchIndex extends AppModel {
var $useDbConfig = 'zendSearchLucene';
}
?>

Saving/Indexing

I’ve tried to keep the datasource functions as simple and familiar as possible. When saving an item to the index, the datasource expects a multidimensional array for each item. For compatibility with CakePHP’s datasource code, the ‘meat’ of the data is nested in the third level of the array. Each sub-array contains information about a field to be stored. For example:

$saveData = array('SearchIndex' => array(
  	'document' => array(
		array(
			'key' => 'name',
			'value' => $record[$Model->alias][$this->settings[$Model->alias]['name']],
			'type' => 'Text'
		),
		array(
			'key' => 'description',
			'value' => $record[$Model->alias][$this->settings[$Model->alias]['description']],
			'type' => 'Text'
		),
		array(
			'key' => 'url',
			'value' => $this->__constructUrl($Model, $record),
			'type' => 'Text'
		)
	)
 ));

Passing that data in a Model::save() call will in turn execute the following Zend code (more or less – this is a very simplified version of the actual ZendSearchLuceneSource saving code):

$index = Zend_Search_Lucene::open('/path/to/the/index/set/in/dbConfig');
$doc = new Zend_Search_Lucene_Document();
foreach ($data as $field) {
$doc->addField(Zend_Search_Lucene_Field::$field['type']($field['key'], $field['value']));
}
$index->addDocument($doc);

Obviously that’s a basic example; you’ll probably send a whole bunch of dynamic info to the indexer. But that’s the gist of it anyway.

Querying

You can search for records just like you would a regular datasource. Pass the search terms as a “query” condition. If you want the search terms to be highlighted in the returned results, pass ‘highlight’ => true in the array of options. Note that only indexed fields will be highlighted.

You can find all results:

function search($term) {
$results = $this->SearchIndex->find('all', array('highlight' => true, 'conditions' => array('query' => 'best cakephp tutorials')));
}

You can mimic Google’s I’m Feeling Lucky with find(’first’):

function search($term) {
$topResult = $this->SearchIndex->find('first', array('conditions' => array('query' => 'best cakephp tutorials')));
}

You can even paginate:

function search($term) {
$this->paginate = array(
'limit' => 10,
'conditions' => array('query' => 'best CakePHP tutorials'),
'highlight' => true
);

$results = $this->paginate();
}

Results are returned in the expected CakePHP way, as a multidimensional array – $results[0]['MyModelAlias'] for multiple records, $results['MyModelAlias'] for one (i.e. with find(’first’)).

There you go – enjoy! As always, comments and suggestions are welcomed.

I used the RSS Feed datasource by Loadsys as a guide to good datasource design. I may have borrowed a function or two. ;)

Neil Crookes’ Searchable plugin also helped.

Adding Better Scope Limiting to CakePHP 1.2’s Tree Behavior

Posted in CakePHP on January 6th, 2010 by Jamie – 6 Comments

OK, so I’ve been working a lot with Cake’s Tree Behavior for the past six or seven months, because a lot of the plugins I’m building at work require complex category structure (i.e. more complex than simple parent -> child). Tree Behavior’s implementation of MPTT is good, but – unless I’m using the behavior incorrectly or can’t find the proper syntax – the ’scope ‘ setting doesn’t really work in the way that I would expect.

Basically, Tree Behavior is great when all of the records in a table fall are part of the same tree. But what if you want different trees within the same table? For example, if you’re making a navigation system, you might have this relationship:

NavigationList hasMany NavigationItem
NavigationItem belongsTo NavigationList

Since navigation can get pretty complex, we might want to use the Tree Behavior to keep track of the ordering of the NavigationItem records. But, we want a separate tree for each NavigationList, so we don’t get our main menu, footer menu, member’s only menu, etc. mixed up. The “scope” setting already exists in Tree Behavior, but it’s basically undocumented, so I had to guess at its syntax (after looking at the Tree Behavior code of course). I tried this:

class NavigationItem extends AppModel {
    var $actsAs = array('Tree' => array('scope' => 'NavList'));
}

… hoping that the behavior would figure out that, when saving the NavigationItem, it should only make changes to the left/right values for NavigationItems with the same nav_list_id as the NavigationItem that’s being saved. But, no such luck; no limit is placed during the save operation, meaning that the entire table’s tree structure gets updated, rather than just those with nav_list_id = the nav_list_id of the NavigationItem we’re saving. So, I just decided to modify the TreeBehavior a bit. Some might whine about not modifying the core, but it’s a fine thing to do if you want more functionality. Just move tree.php out of /cake/lib/models/behaviors and into /app/models/behaviors.

I modified two methods, setup() and beforeSave(), and added my own _addScopeForeignKey() method. The full code is available on Github, but for the lazy the changes I made are:

TreeBehavior::setup() :

function setup(&amp;amp;$Model, $config = array()) {
	if (!is_array($config)) {
		$config = array('type' => $config);
	}
	$settings = array_merge($this->_defaults, $config);

	if (in_array($settings['scope'], $Model->getAssociated('belongsTo'))) {
		$data = $Model->getAssociated($settings['scope']);
		$parent =&amp;amp; $Model->{$settings['scope']};
		$settings['scope'] = $Model->alias . '.' . $data['foreignKey'] . ' = ' . $parent->alias . '.' . $parent->primaryKey;
		$settings['recursive'] = 0;
		$settings['scopeForeignKey'] = $data['foreignKey'];
	}
	$this->settings[$Model->alias] = $settings;
}

Add this line to the top of TreeBehavior::beforeSave() :

function beforeSave(&amp;$Model) {
	$this->_addScopeForeignKey(&amp;$Model);

        ....
}

Add this new function:

/**
 * add a foreign ID scope to the model settings.
 *
 * @param AppModel $Model Model instance
 * @return true on success, false on failure
 * @access protected
 */
	function _addScopeForeignKey(&amp;amp;$Model) {
		if (!isset($this->settings[$Model->alias]['scopeForeignKey'])) {
			return false;
		}

		if (!isset($Model->data[$Model->alias][$this->settings[$Model->alias]['scopeForeignKey']])) {
			return false;
		}

		$this->settings[$Model->alias]['scope'] = $Model->alias . '.' . $this->settings[$Model->alias]['scopeForeignKey'] . ' = ' . $Model->data[$Model->alias][$this->settings[$Model->alias]['scopeForeignKey']];

		return true;
	}

And that’s it. I haven’t tested it all that much but it’s doing the job right now. Comments, suggestions, improvements, polite criticism welcome. :)

Coming soon – CakePHP and the Zend Framework: together at last!

Posted in CakePHP, Zend Framework on January 4th, 2010 by Jamie – 1 Comment

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.

A Better Postal/Zip Code Validation Method for CakePHP 1.2

Posted in CakePHP on December 11th, 2009 by Jamie – 10 Comments

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
        )
    );
}
?>

Hitting the Ice

Posted in Personal on December 10th, 2009 by Jamie – 3 Comments

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:

Finding Random Model Records in CakePHP 1.2

Posted in CakePHP on November 27th, 2009 by Jamie – 2 Comments

There are two main methods for finding random records with PHP (and in this case CakePHP) and MySQL:

  1. Use a SELECT query with ORDER BY RAND() and LIMIT x (where x is the number of results you want).
  2. Get a list of the primary keys of every record in the table, use PHP to select x random entries from that list, and then use a SELECT query using a WHERE primary_key IN (x, y, z) clause.

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:

&lt;?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) &lt; 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);
        }
    }
}
?>

Adding Automatic Query Caching to Model::find() in CakePHP 1.2

Posted in CakePHP on November 24th, 2009 by Jamie – 4 Comments

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:

  • Automatic caching only occurs if debug is set to 0.
  • The updated Model::find() function also implements Matt Curry’s version of custom find methods.
  • After writing this code a few months ago, I came across another implementation of the same idea on End Your If. The two methods are pretty similar, but we came to the results independently. Must mean it’s a pretty good system. ;)

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:

  • Whenever you want to cache a Model::find() result, pass ‘cache’ as one of the method’s arguments.  Pass it as either a string or an array.
    • string: the name used to generate the cache key, which takes the format: model_alias_cache_key
    • array: two valid arguments: ‘name’, used as above, and ‘config’, optionally used to pass the name of the cache config to use. ‘default’ is used if not otherwise specified.
  • And that’s it! Enjoy the speed benefits, but use caching carefully. Pay attention to your cache results; only cache stuff that makes sense (for example, only cache user-specific stuff if you specify the user ID in the cache name).

The code:

&lt;?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;
  }
}
?>