Paginating search results in CakePHP

This post describes my endeavors with paging search results in a recent project. I tend to be very explicit in my posts, so if you prefer you can follow the fast track and jump to the solution

A friend of mine is working on a website for a local realty agent. Since the website will display data about properties that the realtor is handling, I suggested we build it on CakePHP and get some cool features as a bonus. As a bonus he’d learn some of the CakePHP internals. After having set up the database, models, controllers and admin routing and various UI-related matters, we were faced with a very interesting problem:

How do you paginate the results of a search query?

I am sure this has been addressed many times, but I wanted to give a personal taste to the answer. So I read and thought a lot bout this. The real problem I was facing is that the application won’t know about the search query in the subsequent pages of results. This is of course unless you make use of HTTP GET for your search requests. CakePHP also provides nice SEO friendly URL and I didn’t want to give that up. What’s more, HTTP GET is not the right solution because:

Good web development practices demand that HTTP GET requests always present the same data and never alter them.

Search results change with time, so there is no guarantee that the data is the same, even for the same search query. Just imagine for a moment going back to a blog and performing a search for some article’s keywords. You will get that article in your search results, but there may be other (newer) articles containing the same term (let alone a web app where a real estate agent will be updating realties each day). This is why using HTTP GET is considered Bad Practice™ and was out of our line of action.

Since HTTP is a stateless protocol and we cannot or better, choose not to use GET parameters to store the search query, the only way for us to persist data during subsequent requests (different pages of search results) is to use the PHP Session to store the search query.

The Session

The session is a mechanism that wants to make up for HTTP’s stateless nature. A session is unique for every user or, better put, every browser that requests the web pages. Every web programming language offers tools to store, retrieve and delete data relative to a user’s (i.e. browser’s session).

Our plan is to store search query data in the session. CakePHP offers a Session Component that can be used the CakePHP-way. You can even store and retrieve arrays in the session, which does came in handy, as we will see.

The Solution

To make the long story short (or is it to late for that?) the adapted solution is the following:

  1. On first request (POST action), use Session component to store search conditions and then show the first page of search results.
  2. On subsequent requests (GET action), use Session component to retrieve search conditions and hand them over to the paginator.

Before going into the code, I’d like to shed some light into the application structure itself. The controller that’s going to handle the search requests is PropertiesController. PropertyModel is the relative Model.

The search form is simplified to include the following criteria:

  • City of property
  • Type of property
  • Room count
  • Floor count

Data coming from the POST request is stored into Session variables using dot notation. Dot notation (i.e. Properties.floor_count will allow us to retrieve an associative array from the Session (look at line 41). In lines 45-47 we manipulate the associative array retrieved from the session and pass it as a $conditions argument to the paginator (line 52).

<?php
class PropertiesController extends AppController{

	// Session is already included in your AppController
	// this is for illustration purposes only
	$uses = array( "Session", /*...*/ );

	function search(){
		// Make sure some data has been posted
		if( ! empty( $this->data ) ){
			// Delete what was previously stored in the session,
			// this is a new query
			$this->Session->delete('Property');

			// Save search criteria to session
			$city_id = $this->data[ 'Property' ][ 'city_id' ];
			if( $city_id )
				$this->Session->write('Property.city_id', $city_id);

			$type = $this->data[ 'Property' ][ 'type_id' ];
			if ( $type != 0 )
				$this->Session->write( 'Property.type_id', $type );

			$floor_count = $this->data[ 'Property' ][ 'floor_count' ];
			if( $floor_count )
				$this->Session->write( 'Property.floor_count', $floor_count );

			$room_count = $this->data[ 'Property' ][ 'room_count' ];
			if ( $room_count )
				$this->Session->write( 'Property.room_count', $room_count );
		}

		if(  ! $this->Session->check( 'Property' ) ){
			// If no search conditions have been provided
			// set a flash message and redirect to homepage
			$this->Session->setFlash('Provide some search criteria!', true);
			$this->redirect(array('controller'=>'pages', 'action'=>'home'));
		}

		// Now, we are sure to have some search criteria
		$conditions = $this->Session->read( 'Property' );

		// fill up conditions_array
		$conditions_array = array();
		foreach( $conditions as $key => $val ){
			// dot notation for: Model.attribute
			$conditions_array[ 'Property.' . $key ] = $val;
		}

		// Now we provide the search conditions and call the paginator
		$this->paginate  = array(
			'conditions' => $conditions_array,
			'limit' => '2'
		);
		$properties = $this->paginate('Property');
		$this->set(compact('properties'));
		$this->render('search', 'public');
	}
}

After having gone through all this, it seems pretty straightforward. There is one catch: for the purposes of this post, I have not included any additional data sanitizing apart from that provided by CakePHP’s ORM. If you intend to use this code in any real web applications, please read through Data Sanitizing chapter of the CakePHP Book to understand what is sanitized and what’s not.

As a final note, there are also free benefits coming out of this solution, like: Search History and/or Search suggestions for the user, but another post is needed to talk about that.

5 Comments

  • I do not agree with the statement that search queries should not be handled via GET method. If you are willing to store a snapshot of the search as seen in that moment, just insert a date argument in the search.
    Search Results should be linkable just as any other page article to my opinion.

  • Mandi, thanks for sharing your thoughts.

    I agree with your observation.

    In a web application however, like in a an online store, some items disappear and you’d need to have some sort of version info in your DB. Or never delete items but mark them as inactive/unavailable.

    In the studied case, if a property is sold, it should not appear in search results, nor in stored search results. Moreover I dislike HTTP GET for submitting a search request.

    HTTP POST versus GET is a very long discussion and a lot of man hours have been spent writing on this argument ;)

  • Jitendra Rathod wrote:

    Thanks for posting this post. I am facing same type of problem in my admin side paging with the search parameter. In my case I am searching on the base of Email name and status and I want same search parameter on my subsequent pages in cakephp paging. But I am not able to do that by default cake php pagination. I have managed my search parameter in Cookies rather than in session because of I dont want to load on in the session. Please suggest any one better option for that. Also I want my search parameter to be sent through post method.

  • It seems to me that managing the search parameters in Cookies is not a good idea. Unless you are using advanced encryption techniques in Cookies (i.e. client side sessions), they are not reliable (they can be modified easily by the client), using them in your search queries might lead to SQL Query Injection.

    Sending your search parameter via POST requests is a good idea.

    The only other option I see to save your search parameters (but one that I strongly suggest not to use) is to persist them in model in the database. Having said that, I don’t understand why using using Session to manage your keys is such a problem to you?

  • Yes this code work preety fine but what if i want to use LIKE clause on it.?Please help me my work is stucked.

Leave a Reply

Your email is never shared.Required fields are marked *