Pagination with custom find types in CakePHP

With the release of CakePHP 1.2 a whole set of new features were made available to us bakers. One of those features is custom find types, which is one of the coolest things that ever happened since I realized I was cooler than Maverick.
I'm not gonna go through custom find types, you can find more info about them at Matt's blog, or at this article written by someone whose name I think I've heard somewhere. What I'm going to talk about is how to mix your custom find types with pagination, without having to use paginate and paginateCount in your models.
So let's first start by building yet another posts table, and inserting some records:
Code:CREATE TABLE `posts`(
  `id` INT NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(255) NOT NULL,
  `body` TEXT NOT NULL,
  `published` TINYINT(1) NOT NULL default 0,
  `created` DATETIME,
  `modified` DATETIME,
  PRIMARY KEY(`id`)
);
 
INSERT INTO `posts`(`title`, `body`, `published`, `created`, `modified`) VALUES
  ('Post 1', 'Body for Post 1', 1, NOW(), NOW()),
  ('Post 2', 'Body for Post 2', 0, NOW(), NOW()),
  ('Post 3', 'Body for Post 3', 0, NOW(), NOW()),
  ('Post 4', 'Body for Post 4', 1, NOW(), NOW()),
  ('Post 5', 'Body for Post 5', 1, NOW(), NOW()),
  ('Post 6', 'Body for Post 6', 0, NOW(), NOW()),
  ('Post 7', 'Body for Post 7', 1, NOW(), NOW()),
  ('Post 8', 'Body for Post 8', 1, NOW(), NOW()),
  ('Post 9', 'Body for Post 9', 1, NOW(), NOW());

Now let's assume we want a find type called published to fetch only the published posts, and that we also want to be able to paginate using this find type. We will be approaching this through a generic approach, something that can be used throughout all our models. With this in mind, let's first introduce a model based member variable called $_types, where we define the specific needs of each custom find type. Therefore, that variable will hold what we need as conditions, order, etc. for each custom find type. So let's build our Post model:
PHP:<?php
class Post extends AppModel {
    public $name = 'Post';
    protected $_types = array(
        'published' => array(
            'conditions' => array('Post.published' => 1),
            'order' => array('Post.created' => 'desc')
        )
    );
}
?>

As you can see, we define options for each find type as if we would be calling find() directly. So with the above, instead of doing:
PHP:$posts = $this->Post->find('all', array(
    'conditions' => array('Post.published' => 1),
    'order' => array('Post.created' => 'desc')
));

We can now do:
PHP:$posts = $this->Post->find('published');

Now, what if we wanted to paginate with the above custom find type? Just as we set pagination parameters through the controller member variable $paginate, we can specify which find type pagination we'll use. We do so by specifying the find type in the index 0 of the pagination settings. Like so:
PHP:$this->paginate['Post'] = array(
    'published',
    'limit' => 10
);
 
$posts = $this->paginate('Post');

Easy, huh? When this is specified, paginate() does the following:

  1. It issues a find('count') on the Post model, specifying the custom find type (published) in the $options array, through an option named type. Therefore, we can use $options['type'] when our model is about to do the count to use the given options for our custom find type.
  2. It fetches the records by calling find() with the custom find type, find('published') in our example.

So where's that sexy code? Add the following in your AppModel, making the above available for all our models.
PHP:<?php
class AppModel extends Model {
    public function find($type, $options = array()) {
        if (!empty($this->_types)) {
            $types = array_keys($this->_types);
            $type = (is_string($type) ? $type : null);
            if (!empty($type)) {
                if (($type == 'count' && !empty($options['type']) && in_array($options['type'], $types)) || in_array($type, $types)) {
                    $options = Set::merge(
                        $this->_types[($type == 'count' ? $options['type'] : $type)],
                        array_diff_key($options, array('type'=>true))
                    );
                }
                if (in_array($type, $types)) {
                    $type = (!empty($this->_types[$type]['type']) ? $this->_types[$type]['type'] : 'all');
                }
            }
        }
        return parent::find($type, $options);
    }
}
?>

Now ain't CakePHP great? Don't tell me, tell everyone at CakeFest #3.Original post blogged on b2evolution.