Wow, I haven’t posted anything on my blog for a while. I guess I must be busy. A new baby will have that effect. :)

Anyway, I’m happy to release an update to my Copyable behavior, which I shared with everyone a few months ago.

Download the new version on the Github repository.

Along with some general bug fixes, this newest version has a couple of significant bug fixes:

  • Validation is no longer performed before saving the new record. Validation was causing some issues with deep hasMany associations. I don’t see validation as necessary for copying anyway, since the original record already exists.
  • For model associations that can be infinitely nested, such as Page hasMany ChildPage (and then that ChildPage hasMany ChildPage, etc.), copying stops after the first level of containment. These types of associations were causing infinite loops when generating the contain array.

There’s also a new setting, “ignore”, which allows you to specify, via dot (.) notation, contained models that should be ignored.

For example, if a contain array for your model looks like this:

$contain = array(
    'Page' => array('Image'),
    'Log',
    'User' => array('Group' => array('Permission'))
);

You can exclude the “Permission” contain by specifying the ignore setting:

public $actsAs = array('Copyable' => array('ignore' => array('User.Group.Permission')));

Handy for excluding certain assocations that don’t need to be copied, like changelogs and that sort of thing.

Enjoy, and, as always, comments and suggestions welcome!

New theme… again

3 Apr
2010

Trying out a new WordPress theme. My last one got boring fast. One of these days I’ll have enough time to make my own theme… maybe. ;)

It’s been a while since I’ve posted anything of substance, but stay tuned!

UPDATE June 02/10: Please check out the update to Copyable behavior

Until I switched to CakePHP, any CMS I built for a client had a “copy this item” tool. The Cake framework doesn’t have anything like that built in, so for the past year or so the new Cake-powered CMS I built for work hasn’t had any sort of ‘copy item’ ability. Recently, however, I needed the functionality, so I decided to write a behavior to handle it. And now I’m happy to release it to the community as Copyable Behavior.

Download Copyable Behavior on Github.

I’ve wrapped it in a plugin, since that seems to be the trend in the community these days (though I’m not sure why just downloading a behavior file is so bad).

Copyable adds a copy() function to your model, which you can use to copy (that is, create a duplicate of) a record and any of its hasOne, hasMany, or hasAndBelongsToMany relationships. In the case of hasOne and hasMany, those records are recursively copied as well. For example, if you want to copy a LinkCategory that hasMany Link, all of the Link records will be copied. The copy is fully recursive, meaning that if Link HasMany Comment, then all of those records will be copied as well. In the case of HABTM associations, only the join table rows are copied, not the associated records. The copy() function takes one argument – the ID of the record you wish to copy.

Copyable uses Containable to help generate its queries, but don’t worry – it’ll attach Containable if it can’t find it on the model.

A handful of config options:

  • recursive: whether to copy hasOne- and hasMany-associated models (default: true)
  • habtm: whether to copy hasAndBelongsToMany relationships (default: true)
  • stripFields: an array model fields that should ignored when copying (default: id, created, modified, lft, rght)

After attaching Copyable to a model via the $actsAs array – I recommend putting it on AppModel – usage is as simple as:

// From a controller method
$this->MyModel->copy($id);

// From a model method
$this->copy($id);

This is still an early version (I just finished it – and started it – today), but results have been pretty good so far. Suggestions welcome. I’ll probably throw this on the Bakery after it’s been out there for a little while. But for now, it’s on Github.

Yup, I realize that beforeSave() is a little wonky. My fault for not testing before making a major change. Update coming tonight on Github.

Here’s a quickie – a Cipher behavior for CakePHP to handle two-way encryption of sensitive data. If you want to store, say, credit card information, you’ll need a way to retrieve it later; Cake’s built-in security hashing is one-way, meaning that once it’s encrypted it ain’t comin’ back. So, I turned to the Zend Framework and brought in Zend_Filter_Encrypt and Zend_Filter_Decrypt to handle the heavy lifting. This behavior uses mcrypt, so you need to have that PHP extension installed, which (as far as I can tell) is fairly standard.

Download the Cipher Behavior Source on Github

First things first – you need to throw a couple of libraries from the Zend Framework into your vendors folder. Yup, I love my Zend! CakePHP teams up very well with ZF.

You’ll need the following Zend Framework libraries (download the framework):

  • Zend_Filter (Zend/Filter.php and Zend/Filter)
  • Zend_Loader (Zend/Loader.php and Zend/Loader)

Put those files in your vendors folder. You’ll also need to update your include_path somewhere (say, app/bootstrap.php) with the following:

ini_set('include_path', ini_get('include_path') . ':' . APP . '/vendors');

I also recommend defining an autoload function somewhere for Zend classes (again, bootstrap.php is a good choice). A simple, Zend-only autoloader might look like this:

function __autoload($path) {
	if (substr($path, 0, 5) == 'Zend_') {
		include str_replace('_', '/', $path) . '.php';
	}
	return $path;
}

The behavior has three config options:

  • key: the encryption key to use. If null (default), then it’ll use the value of Configure::read(‘Security.salt’).
  • automatic: whether to automatically encrypt and decrypt data as it’s saved/retrieved. Defaults to true.
  • fields: an array of fields belong to the model that should be encrypted

Usage of the behavior itself is straightforward: just add Cipher to the $actsAs array of any model you’d like:

Class User extends AppModel {
    var $actsAs = array('Cipher');
}

Class Order extends AppModel {
    var $actsAs = array('Cipher' => array('fields' => array('credit_card', 'expiry')));
}

You can also manually decrypt and encrypt data, either by passing an array of find results (in which case the fields specified in ‘fields’ will be encrypted/decrypted), or by passing a string:

// Manually encrypt/decrypt the results of Model::find()
$encrypted = $this->encrypt($findResults);
$decrypted = $this->decrypt($encrypted );

// Manually encrypt/decrypt a string
$encryptedSecretString= $this->encrypt('my secret string');
$decryptedString = $this->decrypt($encryptedSecretString);

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.

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.

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!

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.

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. :)

top