There was a lot of positive feedback from the last tutorial which has spurred on a newer, revised tutorial. The contents of this post relate to Cake 1.2 (still in beta, but very usable). Whilst much of the steps are the same, there are a few subtle differences, and I have tried to clean things up a little better. So, without further ado...

Overview

This is a very high level overview of what the tutorial will delve into - nothing too advance, but many people pointed out with the last tutorial that it was a bit hard to keep track of.

  • Livesearch
  • How it's implemented
  • File structure
  • Database, table and Model
  • Controller and views
  • Don't forget the layout

1. Livesearch

If you're not too sure what Livesearch is, you probably don't need to be reading this tutorial. A quick overview of a livesearch would describe how search results are presented to you before you've finished typing - no need to click submit, just type and watch the results appear. Livesearch is part of the Web 2.0 thing, and has come around thanks to the popularisation of AJAX. To see an example of a livesearch in action head on over to Google Suggest.

2. How it's implemented

If you'd rather head straight for the demo (put together by following the steps in this tutorial), then go right ahead.

We will be walking through a simple system of searching a table of first and second names - the controller we'll build later on searches both the first name and the second name field for whatever you type in. Obviously however you wish to implement a livesearch will be slightly different, but the principles are the same.

The basic process of livesearching is as follows:

  • Start typing in an input box
  • Javascript monitors what you type and periodically sends a request off in the background
  • This request is picked up by the controller, which searches the database for matches to your query
  • The controller sends the results to a small template file, which appears under the search box

The mileage is going to vary on this - results could be slow to appear because of a sluggish connection, a large database or an overloaded server. I'm never that keen on websites that implement livesearching when it doesn't add any value to the search. However, for quickly looking up a list of possible matches it is ideal.

3. File structure

I have made the entire CakePHP project tree available for download, but it would be good for you to have some understanding of what the files do before diving in.

The tutorial covers the creation of 1 model, 1 controller, 2 views for the model and 1 stylesheet. The layout is not really important in this scenario, and the one included in the project tree is very, very simple.

The model simply maps the database table (created in the next step) into CakePHP. The controller is responsible for directing the visitor to the relevant views, and of course handling the search request. It is quite surprising how little code there is in the controller.

The first view displays the search box and includes the AJAX functionality necessary to fire off the search query to the controller. It also contains a <div> element which is populated by the results. The second view is a simple template for formatting the search results. In this case it is just an unordered list.

From here on in things get very specific to the tutorial.

4. Database, table and Model

I'm assuming you have an operational CakePHP environment, and the database is all setup and ready to go. If this is not the case, you'd better get yourself over to the excellent CakePHP manual and brush up.

We are going to create one table (people) and put a few records into it:

CREATE TABLE `people` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name_first` varchar(20) DEFAULT NULL,
`name_second` varchar(20) DEFAULT NULL,
PRIMARY KEY  (`id`)
);

Now we can populate it with a handful of British Prime Ministers:

INSERT INTO `people` (`id`,`name_first`,`name_second`) VALUES ('7','James','Callaghan');
INSERT INTO `people` (`id`,`name_first`,`name_second`) VALUES ('6','Margaret','Thatcher');
INSERT INTO `people` (`id`,`name_first`,`name_second`) VALUES ('1','Tony','Blair');
INSERT INTO `people` (`id`,`name_first`,`name_second`) VALUES ('5','John','Major');
INSERT INTO `people` (`id`,`name_first`,`name_second`) VALUES ('8','Edward','Heath');
INSERT INTO `people` (`id`,`name_first`,`name_second`) VALUES ('9','Harold','Wilson');
INSERT INTO `people` (`id`,`name_first`,`name_second`) VALUES ('10','Alec','Douglas-Home');
INSERT INTO `people` (`id`,`name_first`,`name_second`) VALUES ('11','Harold','Macmillan');
INSERT INTO `people` (`id`,`name_first`,`name_second`) VALUES ('12','Anthony','Eden');
INSERT INTO `people` (`id`,`name_first`,`name_second`) VALUES ('13','Clement','Attlee');
INSERT INTO `people` (`id`,`name_first`,`name_second`) VALUES ('14','Winston','Churchill');

The final step is to create a model which CakePHP will know to map to the table we just created:

<?php
/* File: /app/models/person.php */
class Person extends AppModel
{
    var $name = 'People';
}
?>

The next steps are where something actually starts to happen.

5. Controller and views

In order to interact with the model we just created, there has to be a controller associated with it. If you've got to this point and have no idea what a model, view or controller is, it's time you got yourself over to Wikipedia to read up. Our controller is only going to have two actions (and two associated views), so it's all pretty simple:

<?php
/* File: /app/controllers/people_controller.php */
uses ('sanitize');
class PeopleController extends AppController
{
    var $Sanitize;
    var $name = 'People';

    function index() { }

    function beforeFilter()
    {
        $this->Sanitize = new Sanitize();
    }

}
?>

CakePHP's magic will automatically associate the model Person (singular) to the controller People (plural), so you don't have to worry about that.

Now we need to tell CakePHP to use the JavaScript and AJAX helpers that will give us the ability to fire off a livesearch request and do various displaying/hiding magic. Add the following line below the $name declaration:

var $helpers = array('Html', 'Javascript', 'Ajax');

It is important to also include the Html helper, as CakePHP will override the default helper inclusions with what you specify here. It's also worth noting that you can set default helper inclusions in /app/app_controller.php, but we're keeping everything wrapped up in one controller, here.

The final step of constructing the controller is to process a search request. The second view created in this step (further down) contains a form that will be used to submit data to our search action. This action will be responsible for fetching the results accordingly, setting the layout to a CakePHP-provided Ajax layout and passing the results to the search results view (created after this step):

<?php
function search() {
if (!empty($this->params['form']['query']))
{
$query = $this -> Sanitize -> paranoid($this->params['form']['query']);
if (strlen($query) > 0)
{
    $result = $this -> Person -> findAll("name_first LIKE '%".$query."%' OR name_second LIKE '%".$query."%'");
    $this->set('result', $result);
    $this->layout = 'ajax';
}
}
}
?>

There are a few sanity checks here. Firstly we sanitize the query data to prevent any SQL injection style attacks - this is done through the inbuilt Sanitize library, which helps strip out any nasty characters. Secondly we want to make sure that the query field from the view is not empty, and that it is of a sensible length (greater than 1 here, just for demonstration purposes). If these checks pass, the results from the query get sent onto the results view.

The first view we'll take a look at is the one that will format the search results - we're going to call this search.ctp and it will live in /app/views/people/ - you may need to create the subdirectory if you haven't already. Copy the following into the contents of the file:

<?php
/* File: /app/views/people/search.ctp */
if (isset($result)) {
print '<ul>';
foreach ($result as $user)
{
    $display = $user['Person']['name_first'] . ' ' . $user['Person']['name_second'];
    print '<li>';
        print '' . $display . '';
    print '';
}
print '</ul>';
}
?>

As you can (hopefully) deduce, this checks for the presence of a $result variable and populates an ordered (a.k.a. numbered) list with the contents, and spitting out the first and second name of each result.

The second view is somewhat more complicated, and initiates the various Javascript related functions needed for the Ajax-powered livesearching. This view is stored in index.ctp, and should look something like the following:

<?php /* File: /app/views/people/index.ctp */?>
<form accept-charset="UNKNOWN" enctype="application/x-www-form-urlencoded" method="get">
<input id="query" maxlength="2147483647" name="query" size="20" type="text" />
<div id="loading" style="display: none; "><!--p echo $html--> image("spinner.gif") ?></div>
</form>

<?php
$options = array(
    'update' => 'view',
    'url'    => '/people/search',
    'frequency' => 1,
    'loading' => "Element.hide('view');Element.show('loading')",
    'complete' => "Element.hide('loading');Effect.Appear('view')"
);

print $ajax -> observeField('query', $options);
?>

<div id="view" class="auto_complete">
    <!-- Results will load here -->
</div>

The first thing to note here is the form. This is a very, very simple chunk of HTML that gives us one input box - no location to submit the form to, no submit button; just the input box. Next to that is the spinner graphic (you all know what this is...) that we want to toggle display of depending on what the Ajax handler is up to.

The $options array could well be integrated into the statement below it, but this makes things a bit more readable. The observeField is a function provided to us by the Ajax helper that will watch a form field and fire off an Ajax request when it changes. The $options array has all sorts of information about the request to make, and what to do when it's being made...

...The update field tells the Ajax handler where to display the results it receives. In this case, they are going into the DIV at the bottom of the view.

The url field tells the Ajax handler where to submit the contents of the watched field to. This is the action we created earlier.

The frequency field tells the Ajax handler how often it should re-check the watched field and submit the contents to the url specified above. This is in seconds.

The loading and complete fields tell the Ajax handler what it should do when it's waiting for results and what to do when it loads them. The values assigned to these are script.aculo.us calls. In this instance it will show the spinner graphic when results are being fetched, and will hide it again and display the results when it has got everything it needs.

Don't forget the layout

The final obstacle to having a working livesearch (from following this tutorial), is the layout. The first thing you'll need to do is download the prototype and scriptaculous libraries - you can get them from here. You'll need to extract the scriptaculous.js and prototype.js to /app/webroot/js.

I opted to use a very simple layout, so as not to distract from what was going on. You're welcome to use the following, or create your own. Whichever way you go, do not include the Javascript and stylesheet includes!

<?php /* File: /app/views/layouts/default.ctp */
    e ($html -> css('screen'));
    e ($javascript -> link('prototype'));
    e ($javascript -> link('scriptaculous'));

    print $content_for_layout;
?>

The stylesheet is, again, very simple: (if you're using this, save it to /app/webroot/css/screen.css)

* { font-family: Verdana, sans; }
a { color: #000000; }
a:hover { color: #FF0000; }
p { font-size: 12px; }
div { display: inline; font-size: 12px;}
input { padding: 4px; }
li { margin: 5px; }
div.auto_complete { display: block; }

That's it for the tutorial. You might want to check out the demo of everything you seen above in action. You can also download the CakePHP project tree - this contains a vanilla CakePHP 1.2 setup and everything you see above. There is also a SQL dump file in there to speed up table creation & population.

If you think I've missed anything important - explanation, technical detail or otherwise then please comment below or send me an email (address is over on the left in the sidebar). There was a good deal of feedback from the last tutorial, so thanks to all.