Wednesday, July 9, 2008

Passing Named Variables To PaginatorHelper (aka Filtering Paged Data)

It came to pass that an application had a table with lots of similar results. For me, it was a list of serials on combination locks. Several thousand of them, in fact. Since the users will typically want to look for a lock with a specific serial, sorting was woefully inadequate. I needed to filter.

But how?

The CakePHP 1.2 manual indicates that the Controller->paginator variable will accept the options from the now deprecated function Model->FindAll. However, by default, it doesn’t seem to retain this information across page loads.

In other words, the moment I click on one of the PaginatorHelper auto-generated links, my filter goes away. This means that we need to find a way to pass a custom defined variable in the URL, which I did in my Locks controller, like so:

function index() {
    if (isset($this->data['Lock']['serial']))
        $this->params['named']['filter'] = $this->data['Lock']['serial'];
    if (isset($this->params['named']['filter']) ) {
        $this->paginate['conditions'] = array(
            'Lock.serial LIKE ' => '%' . $this->params['named']['filter'] . '%'
        );
    }

    $locks = $this->paginate('Lock');
    $this->set(compact('locks'));
    if (isset($this->params['named']['filter']) )
        $this->params['paging']['Lock']['options']['filter'] = $this->params['named']['filter'];
}

The first two lines of the function simply check for a search parameter specified from a form in the index view defined by the lines:

$form->create('Lock', array('action'=>'index'));
echo $form->input('serial', array('label'=>'Serial Contains: '));
echo $form->end('Apply Filter');

The next two lines apply the filter, regardless of whether it was passed by the form or the URL.

The two lines after that generate the paginated results, but nothing we have done so far will preserve the filter across page loads.

That’s where my final if statement comes into play, which assigns our designated filter (sans my additional SQL markup, to keep the user interface clean) to a variable that is preserved. Of course, $this->params['paging']['Lock']['options']['filter'] is a bit of a mouthful, so I’ll break that down.

From generous use of the debug() function, it appears that Cake stores the information needed for the PaginatorHelper to generate its help in the view in Controller->params['paging']. Each relevant model is stored as a sub-array (indicating that we can filter on multiple models if necessary). Each model contains several elements: page, current, count, prevPage, nextPage, pageCount, defaults and options.

Array
(
  [Lock] => Array
    (
      [page] => 2
      [current] => 3
      [count] => 13
      [prevPage] => 1
      [nextPage] =>
      [pageCount] => 2
      [defaults] => Array
        (
          [limit] => 10
          [step] => 1
          [order] => Array
            (
              [Lock.serial] => asc
            )
          [conditions] => Array
            (
              [Lock.serial LIKE ] => %100%
            )
        )
      [options] => Array
        (
          [page] => 2
          [limit] => 10
          [order] => Array
            (
              [Lock.serial] => asc
            )
          [conditions] => Array
            (
              [Lock.serial LIKE ] => %100%
            )
          [filter] => 100
        )
    )
)

It’s intuitive how PaginatorHelper uses most of these values, although I haven’t seen (or looked for) a clear raison d’etre for [defaults]. The elements of options, save for [conditions], are where CakePHP stores the values that get defined in the links. Hence, with this code, my Cake-generated links now look like:

appserver/app/locks/index/page:1/filter:100

Had I added the variable bob and assigned it the value 'good', the options array would look (internally) like:

[options] => Array
(
  [page] => 2
  [limit] => 10
  [order] => Array
    (
      [Lock.serial] => asc
    )
  [conditions] => Array
    (
       [Lock.serial LIKE ] => %100%
    )
  [filter] => 100
  [bob] => good
)

and the link like:

appserver/app/locks/index/page:1/filter:100/bob:good

Even better, since this entire array is passed to the view, we can look for these custom array elements in the view to indicate whether or not a filter has been applied and if so, how.

No comments: