Finding neighbors with CakePHP
There's a benefit to using CAKEPHP to handle all your SQL as opposed to you providing your own, even for complex SQL statements. First of all, you do not have to worry about SQL injection since Cake can sanitize and escape your data for you. You can use pagination with the results and its handled cleanly by the framework. Your program code is more portable and database independent since translation tables take care of specific database type differences such as BOOL vs. TINYTINT etc. Encapsulation is easier when you only have to deal with all your queries through one method - 'find'. And this method can handle most SQL type requests. It may easily be extended to handle more specific types of queries as well. Over the next few days I'll document various ways of building complex queries using pure CAKE and no SQL. Today we'll look at finding neighbors using CAKEPHP.
Before we move forward, please note that the code below has been tested with CAKE 1.2.0.7296 RC2 which is still a release candidate. So there are no guarantees that this will work for all future versions. That said, the 'find' method is quite flexible in receiving parameters. The 'right' way of using find is:
/** * @type - String - which defines an inbuilt or custom type * @conditions - Array - associative array with keys such as fields,
* conditions, order etc. */ Controller->model->find($type, $conditions);
The following inbuilt types are available:
- all - $model->find("all", $conditions);
- first - $model->find("first", $conditions);
- count - $model->find("count", $conditions);
- neighbors - $model->find("neighbors", $conditions);
- list - $model->find("list", $conditions);
- threaded - $model->find("threaded", $conditions);
The first three are obvious. They find all matches, the first match or the count of results respectively. Let's look at neighbors first and we'll look at list and threaded in the next few days.
The neighbors
Let's say you're interested in finding a previous and a next entry for a given 'Post' (where Post is your model). Your controller action could look like this:
/*action in your controller*/ public function get_neighbors($id=null){ if (!empty($id)){ $this->Post->id = $id; $neighbors = $this->Post->find("neighbors"); //Pass the neighbors to the view $this->set("neighbors", $neighbors); } }
Presuming the given $id was the numeric value 12, this will automatically generate SQL something on these lines:
SELECT * FROM `posts` AS `Post` WHERE `Post`.`id` >= 11 AND `Post`.`id` != 12 ORDER BY `Post`.`id` ASC LIMIT 2;
Right!, that's cool but what if you're looking at finding only published neighbors (not drafts) which have a publish date no later than 'today' (meaning skip the posts that have a publish date set for the future ). That brings us to the second parameter of find with 'conditions'.
Conditions
The optional conditions for a find method call are given as an array with the following keys (all keys are optional)
- fields - The fields to search for (This saves you from returning useless columns)
- conditions - The conditions to apply to the query
- limit - How many results do you want to get?
- recursive - For linked models, do you want to query other model tables as well?
- group - GROUP BY a particular field
- order - ORDER BY a particular field
- offset - Offset the result by 'n' number of results
- page - A specific page number. Pagination can automatically deal with this
- joins - A custom join with another table. It is better to use the model class to create a belongsTo, hasOne, hasMany or hasAndBelongsToMany relationship with another model, than mess with a manual join. Creating the relationship in the model automatically manages the required joins based on your recursive key value.
So to meet our conditions we'd write the above action like this:
/*action in your controller*/ public function get_neighbors($id=null){ if (!empty($id)){ //set the primary key for the neighbor $this->Post->id = $id; //create conditions $findConditions["fields"] = array("Post.id", "Post.title"); $findConditions["conditions"] = array(); $findConditions["conditions"]["Post.publish"] = 1; $findConditions["conditions"]["Post.pubdate <="] = date("Y-m-d H:i:s"); $findConditions["recursive"] = -1; //get the neighbors $neighbors = $this->Post->find("neighbors", $findConditions); //Pass the neighbors to the view $this->set("neighbors", $neighbors); } }
And viola, you have a result that looks like this:
Array
(
[prev] => Array
(
[Post] => Array
(
[id] => 10
[title] => I am the Previous Post
)
)
[next] => Array
(
[Post] => Array
(
[id] => 12
[title] => I am the Next Post
)
)
)
Using the result keys "prev" and "next" you may display the results any way you want.
Next, I'll document the use of "List" and "Threaded" types for the find method along with the context where using them is cake-walk.





5 responses
Have your say
Thank you for this information, its worth noting that specifying the 'id' is mandatory else cakephp will not be able to build the necessary query.
$this->User->find("first", array('conditions'=>array("User.name"=>'value')));
Great explanation, found cookbook's documentation for this functionality to be lacking. Is there a built in way of retrieving the last record if ['prev'] is empty?
it gets really interesting, if you have UUIDS as primary keys (char 36) and want to find the neighbors.
The entries are sorted by "created" timestamp. I did some coding on my own and came up with:
function neighbors($id, $options = array(), $qryOptions = array()) {
$sortField = (!empty($options['field'])?$options['field']:'created');
$normalDirection = (!empty($options['reverse'])?false:true);
$sortDirWord = $normalDirection?array('ASC','DESC'):array('DESC','ASC');
$sortDirSymb = $normalDirection?array('>=','<='):array('<=','>=');
if (is_array($id)) {
$data = $id;
$id = $data[$this->alias]['id'];
} else {
$data = $this->find('first', array('id'=>$id));
}
if (empty($id) || empty($data) || empty($data[$this->alias][$sortField])) {
return false;
} else {
$field = $data[$this->alias][$sortField];
}
$findOptions = array();
$findOptions['conditions'][$this->alias.'.id !='] = $id;
$return = array();
$options = $findOptions;
$options['conditions'] = am($options['conditions'],array($this->alias.'.'.$sortField.' '.$sortDirSymb[1] => $field));
$options['order'] = array($this->alias.'.'.$sortField.'' => $sortDirWord[1]);
$this->id = $id;
$return['prev'] = $this->find('first', $options);
$options = $findOptions;
$options['conditions'] = am($options['conditions'],array($this->alias.'.'.$sortField.' '.$sortDirSymb[0] => $field));
$options['order'] = array($this->alias.'.'.$sortField.'' => $sortDirWord[0]);
$this->id = $id;
$return['next'] = $this->find('first', $options);
return $return;
}
Thanks! Good post.
I had trouble using find(neighbors) in connection with i18n, but now it works great! :-)