Custom Pagination in CakePHP

To continue on last weeks thought of a “lite” forum, I needed 2 tables (Forums, Posts). Since this is a “lite” forum, I did not want to create a mid-table labeled topics, so I incorporated that in the Posts table. The other reasoning behind this, is that to create a hybrid forum/blog, the topic is really just a beginning post in the thread, so keep those in the Posts table, just mark it as a topic to differentiate this from the other posts.

I created two controllers, forums_controller.php and posts_controller.php. All of the links on the application will point to the forums_controller.php file. The models need to be created, forum.php and post.php, with the relationships.

The file user.php (User model) needs to have a “hasMany” relationship with Posts.

var $hasMany = array(
	'Post' => array('className' => 'Post',
		'foreignKey' => 'post_id',
		'dependent' => false,
	)
);

The Forum model needs a “hasMany” relationship with the Post model

var $hasMany = array(
	'Post' => array('className' => 'Post',
		'foreignKey' => 'forum_id',
		'dependent' => false,
	)
);

The Post model needs a “belongsTo relationship with both the Forum and User models.

var $belongsTo = array(
    'Forum' => array('className' => 'Forum',
        'foreignKey' => 'forum_id',
        'conditions' => '',
        'fields' => '',
        'order' => ''
    ),
    'User' => array('className' => 'User',
        'foreignKey' => 'user_id',
        'conditions' => '',
        'fields' => '',
        'order' => ''
)
);

Now that these are completed, it is time to look at the pagination. Blind pagination will just pull the entire model and its relationships. Thus meaning, that if I were to do a blind pagination, as the Bake method does, it would list all entries in the Post table, including posts marked as topics, posts that belong to other forums, etc. Since I am not interested in paginating the Forum model, we need to address this in the Post model.

Since a full customized pagination is not needed, I need to alter the blind pagination and call just those posts needed. To do this, there are a few additions that need to be made to the forums_controller.php file. Near the top where all the vars are called, I entered:

var $paginate = array(
	'Post'	=> array(
		'limit'	=> 2,
		'page'	=> 1,
		'order'	=> array(
			'Post.created'	=> 'asc')
	)
);

This sets the variables to use for the paginator object. I am setting the limit to 2 right now, so I can test the links in the view, I will change that to 15 or 20 (depending on final pass with the customer). I am also setting a sort order. Now I will go to the “viewtopics” action I created.

function viewtopic($forum = null, $topic = null) {
 ...
}

I need the forum_id and the $topic_id to build correct queries, links, etc. To set up the pagination for this, I need to set a conditions variable.

$cond = array('Post.parent_topic' => $topic);

The $topic variable is the “parent_topic” post. This will grab all of the posts where the parent_id == $topic. But I should also grab the “parent” (topic post) so I can adjust the the $cond var to be:

$cond = array('OR' =>array('Post.parent_topic' => $topic, 'Post.post_id' => $topic));

This will find all posts for this topic, by using “WHERE (`Post`.`parent_topic` = 3) OR (`Post`.`post_id` = 3)”. Now set the paginator object and view variable. To do this, I need to call the Model object I want to paginate, and the conditions variable I just set.

$this->set('posts', $this->paginate("Post", $cond));

This now grabs all of the right posts, and creates an array with the results and ready to paginate in the view. Since this function relies on 2 parameters from the URL, we will need to adjust the links in the view.

To view all the posts, I created a viewtopic action, please follow the documentation in the CakePHP manual. This basically sets up the total number of results, how many pages, and the navigation for each page. At the top of my viewtopic.ctp page I have

echo $paginator->counter(array(
'format' => __('Page %page% of %pages%, showing %current% records out of %count% total, starting on record %start%, ending on %end%', true)
));

At the bottom of the view, I have the following:

<div class="paging">
	<?php echo $paginator->prev('<< '.__('Previous Page', true), array(), null, array('class'=>'disabled'));?>
 | 	<?php echo $paginator->numbers();?>
	<?php echo $paginator->next(__('Next Page', true).' >>', array(), null, array('class'=>'disabled'));?>
</div>

(Yours may differ slightly, and that is ok). This sets the view up.

The code at the top is ok, as that is not going to have any problems. However, the bottom navigation is a little broken, as I need it to point to: example.com/forums/viewtopic/1/3/page:2. But it points to example.com/forums/viewtopic/page:2, which will create an error because those parameters are not in the URL. The paginator functions as listed above do not pass in the parameters, so we need to add those in.

The Paginator API documentation has a list of the different functions available. Using these, and viewing the actual code for those functions is also very helpful. But to break these down:
prev ($title= ‘<< Previous', $options=array(), $disabledTitle=null, $disabledOptions=array())
numbers ($options=array())
next ($title= 'Next >>’, $options=array(), $disabledTitle=null, $disabledOptions=array())

The prev() and the next() functions have four parameters, the title for the link, options to set, title when the link is disabled, and options for the disabled pagination link. The numbers() function has only one, which is the $options. This is the one we need to work on to get the links working. However, the options requires a specific set of keys, which can be found at the API source code. The one we are going to focus on is, “$options['url'] - Url of the action”.

To update the code above to pass in the correct forum, and topic, we need to alter the prev tag as follows:

$paginator->prev('<< '.__('Previous Page', true), array('url' => $paginator->params['pass']), null, array('class'=>'disabled'));?>

Just so that it is more readable, I am going to split this up:

‘<< '.__('Previous Page', true)
This is the title parameter
array(’url’ => $paginator->params['pass'])
this is the options section I modified. It is passing in the ‘url’ key, then passing the params from the $paginator object (which hold any type of parameters from the link). So if there are 2 parameters like in this example, or 10, it will get all of those.
null
title link disabled, no change
array(’class’=>’disabled’)
disabled options link, which we kept at the same.

So by just copying and pasting the following
array(’url’ => $paginator->params['pass'])
in the prev, next and numbers functions, it will paginate the results, and put in the correct forum and post ids in the link.

And just like that, the pagination is complete for a semi-custom pagination of forum/posts results. It may not exactly match up any other ones, but it will work. There may also be short cuts to this as well, and if you know of any, then please mention them.