Using a custom collection class with Zend_Paginator

Zend_Paginator is a Zend Framework component for paginating collections of data–for example, breaking a list of blog posts or search results into multiple pages. The easiest way to use Zend_Paginator is by passing it a Zend_Db_Select object, which lets it automagically modify the select query to fetch only the results for the desired page.

But designing your models to work with Zend_Paginator in this way can be messy and inelegant.  It often involves either creating a Zend_Paginator instance inside your model or data mapper, or else passing a select object back out of your data mapper. Either approach seems clunky, and violates the encapsulation of your models.

A cleaner approach is to fetch a collection of your models in a custom collection class, and then pass that collection directly to Zend_Paginator. You can write your collection class in such a way that it only fetches data as needed, handling large result sets just as efficiently as passing a select object directly to Zend_Paginator.

Read on to see how.

Say you have an action that shows a paged list of users. The goal is to be able to do something like this in your action controller:

$user_mapper = new Model_User_UserMapper();
$users = $user_mapper->findAll();

$paginator = new Zend_Paginator($users);
$paginator->setItemCountPerPage(20);
$paginator->setCurrentPageNumber($this->_getParam('page'));

$this->view->users = $paginator;

…and this in your view script:

<?php
if (count($this->users) > 0) {

  foreach ($this->users as $user) {
    echo $user->name . '<br>';
  }

  echo $this->paginationControl($this->users, 'Sliding',
    'my_pagination_control.phtml');
}

For this to work, $users needs to be an instance of a collection class that implements the following interfaces: Zend_Paginator_Adapter_Interface (so it can be passed to Zend_Paginator), Iterator (for foreach()), and Countable (for count()).

See below for an example of such a collection class (or fork it at Github).

[github_cv url='https://github.com/jasongrimes/zf-simple-datamapper/blob/master/Jg/Mapper/Collection.php']

Jg_Mapper_Collection stores an array of “raw” user data, as retrieved from the database. When a row is accessed (ex. by iterating over the collection with foreach()), _getRow() asks the mapper to convert the raw data for that row into a user object, which it returns.

The user mapper is used to fetch the user objects to and from the database. In this example, the Model_User_UserMapper class needs to have at least the following methods:

createObject() takes an array of data and returns a user object populated with that data:

public function createObject(array $data) {
  $class = $this->getDomainObjectClass();
  return new $class($data);
}

getDomainObjectClass() returns the name of the user class. It’s used by the collection class for type checking.

public function getDomainObjectClass() {
  return 'Model_User';
}

findAll() fetches all the users from the database, and returns them in a collection.

public function findAll() {
  $select = $this->_getDbAdapter()->select()->from('mydb.users');
  $data = $select->query()->fetchAll();
  return new Jg_Mapper_Collection($data, $this);
}

More detailed sample code is available in my zf-simple-datamapper project at Github.

This works fine for small collections, but what if you have a million users? It’s not efficient to fetch a huge result set and store it all in the collection object when you may end up needing only a few pages of it.

To handle this properly, let’s extend the collection class to support the lazy load pattern, so it only fetches the raw data for the necessary pages.

[github_cv url='https://github.com/jasongrimes/zf-simple-datamapper/blob/master/Jg/Mapper/LazyCollection.php']

When an instance of Jg_Mapper_LazyCollection is created, instead of passing it an array of “raw” data from the database, the mapper passes it a Zend_Db_Select object instead. This select object is used to retrieve data as needed, modified by an appropriate LIMIT clause. The select object is also modified to automatically generate a count of the total number of results. This logic can be complicated, so we use the count() method of Zend_Paginator_Adapter_DbSelect as a helper to handle it.

In order to support lazy collections, the finder method in the mapper needs to be updated. Here’s the new findAll() method:

public function findAll($options = array()) {
  $select = $this->_getDbAdapter()->select()->from('mydb.users');

  if ($options['get_lazy_collection']) {
    return new Jg_Mapper_LazyCollection($select, $this);
  } else {
    $data = $select->query()->fetchAll();
    return new Jg_Mapper_Collection($data, $this);
  }
}

When fetching a result set that you intend to pass to Zend_Paginator, tell the finder method to return a lazy collection, like this:

$users = $user_mapper->findAll(array('get_lazy_collection' => true));

For more detailed sample code, see this zf-simple-datamapper project at Github.

10 thoughts on “Using a custom collection class with Zend_Paginator”

  1. You are writing tech posts now?! I’ve been living in the dark ages, didn’t know you had a blog :-)

    This is all find and well but it seems like you are still fetching all the users and then doing the paging in memory, no? Doesn’t your ORM have a way to provide paged results directly from the database?

    1. Hey Boyan,

      The LazyCollection class modifies the select object with a limit clause, so it only fetches the slice of data for the current page (in the _loadRawSlice() method).

      I hear that Zend Framework 2 will be more tightly integrated with Doctrine, so maybe Zend_Paginator will have a native adapter for Doctrine collections. This article shows a lightweight example of doing it without a full-blown ORM.

  2. I like the valuable information you provide in your articles. I will bookmark your blog and check again here regularly. I am quite certain I’ll learn many new stuff right here! Best of luck for the next!

  3. Hi,
    I have a question: I don’t understand how using the lazy collection, the lazy_collection class knows which offset and limit must use.

    Thanks.

    1. Hi Carlos,

      Zend_Paginator’s paginationControl passes the offset and limit to LazyCollection::getItems(). This offset and limit is passed along to _loadRawSlice(), which executes the SQL query with the appropriate limit parameters.

      Does that make sense?

      Thanks,

      Jason

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>