Let’s help out CakePHP’s pagination

While it would be sweet, if the paginated data was usually not much more involved than a simple SELECT * FROM some_table… Unfortunately it is not.

In many cases there are a few, JOINs to related models, and likely a GROUP BY statement somewhere (maybe DISTINCT ?, who knows).

What I am getting at, is an SQL issue if that’s what you’d call it…

count(*) (which CakePHP uses properly, indeed) will yield pretty strange results if you have JOINs and GROUP BY (or any variation thereof… or “worse” yet, things like HAVING, etc.).

The display of records to the end-user will “disagree” with the number, which count(*) produces.
This is because count(*) actually counts everything (meaning across various models in the case of a JOIN).
The main issue comes up when one attempts to paginate the result-set from a complex query. You might see only 20 records displayed due to GROUP BY, but count(*) will actually return 43 (for example) because it does not actually count in the same way SELECT returns the records.

However, with a little help this problem is quite easily resolved…

Remember, we can always override the paginateCount() method inside a model.

For example, if we have an Order Model, just to get the expected count of records we’ll add:

public function paginateCount($conditions = null, $recursive = 0, $extra = array()) {
    if($this->statusId) {
      $conditions = array('conditions' => array('Order.order_status_id' => $this->statusId));
    }
    $results = $this->find('count', array_merge(array('recursive' => -1), $conditions));
  	return $results;
  }

With this scenario, we can get an accurate count of all orders, or just the ones that have some specific status.

The bottom line is that we know exactly what needs to be counted, and overriding paginateCount() allows us to do just that, regardless of what the actual record fetching query is like.

Make updateAll() fire behavior callbacks

For a while now updateAll() would not trigger any of the model’s behavior callbacks.

This presents a problem for a couple of reasons:

  1. The updateAll() method allows to easily do things like field_value = field_value + 1, because unlike other similar methods it does not escape fields/values
  2. Secondly we cannot rely on proper Behavior execution, which can lead to some unexpected results

What we have below is a simple override (and a very rough patch at this point) that will trigger afterSave() of the attached behavior as expected.

Take the code below and place into the model, which uses updateAll() at some point.

public function updateAll($fields, $conditions = true) {
    $db =& ConnectionManager::getDataSource($this->useDbConfig);
    $created = FALSE;
    $options = array();
    if($db->update($this, $fields, null, $conditions)) {  
      $created = TRUE;      
      $this->Behaviors->trigger($this, 'afterSave', array($created, $options));
      $this->afterSave($created);
      $this->_clearCache();
      $this->id = false;
      return true;
    }
  return FALSE;
 }

Again, this works for my immediate needs and doesn’t break any tests. However there are a few things that can be improved to make it more consistent with other methods like save(), for example.

Well, either way I am hoping that this will help someone.

Quickly grant Auth access to multiple controllers

Common way to allow Auth access to all actions in a controller is to do something like this:

//In some controller 

public function beforeFilter() {    
  $this->Auth->allow('*');
  parent::beforeFilter();
}

However it can get pretty tedious if you’ve got lots of controllers and have to go through a bunch of them to enable (or disable) access.

Instead, try something like this in your AppController:

public function beforeFilter() {    
  $this->Auth->loginAction = array('controller' => 'users', 'action' => 'login');
  $this->Auth->loginRedirect = array('controller' => 'users', 'action' => 'index');    
  $this->allowAccess();
}
  
private function allowAccess() {
  if(in_array($this->name, array('Pages'))) {
    $this->Auth->allow('*');  
  }
}

The above will let Auth to access everything in the PagesController.
If you need to grant access to additional controllers simply add them to the array of names:
array('Pages', 'Books', 'Customers', 'Etc')

Having to deal with a single file to grant/deny access just makes things easier…
You could even make that “grantable” array a property of the AppController.