Image may be NSFW.
Clik here to view.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).
<?php /** * A simple collection class that also works with Zend_Paginator. * * @author Jason Grimes <jason@grimesit.com> * @link https://github.com/jasongrimes/zf-simple-datamapper * @category Jg * @package Mapper * @copyright Copyright (c) 2011 Jason Grimes <jason@grimesit.com> */ class Jg_Mapper_Collection implements Iterator, Countable, Zend_Paginator_Adapter_Interface { protected $_mapper; protected $_total; protected $_raw = array(); protected $_result; protected $_pointer = 0; protected $_objects = array(); public function __construct(array $raw, Jg_Mapper $mapper) { $this->_raw = $raw; $this->_total = count($raw); $this->_mapper = $mapper; } public function add($object) { $class = $this->_mapper->getDomainObjectClass(); if (!($object instanceof $class)) { throw new Jg_Mapper_Exception('This is a "' . $class . '" collection'); } $this->_objects[$this->count()] = $object; $this->_total++; } protected function _getRow($num) { $this->_notifyAccess($num); if ($num >= $this->_total || $num < 0) { return null; } if (!isset($this->_objects[$num]) && isset($this->_raw[$num])) { $this->_objects[$num] = $this->_mapper->createObject($this->_raw[$num]); } return $this->_objects[$num]; } /** * Rewind the Iterator to the first element. Required by Iterator interface. * * @link http://php.net/manual/en/iterator.rewind.php * @return void Any returned value is ignored. */ public function rewind() { $this->_pointer = 0; } /** * Return the current element. Required by Iterator interface. * * @link http://php.net/manual/en/iterator.current.php * @return mixed Can return any type. */ public function current() { return $this->_getRow($this->_pointer); } /** * Return the key of the current element. Required by Iterator interface. * * @link http://php.net/manual/en/iterator.key.php * @return scalar scalar on success, integer 0 on failure. */ public function key() { return $this->_pointer; } /** * Move forward to next element. Required by Iterator interface. * * @link http://php.net/manual/en/iterator.next.php * @return void Any returned value is ignored. */ public function next() { $row = $this->_getRow($this->_pointer); if ($row) { $this->_pointer++; } return $row; } /** * Checks if current position is valid. Required by Iterator interface. * * @link http://php.net/manual/en/iterator.valid.php * @return boolean The return value will be casted to boolean and then evaluated. * Returns true on success or false on failure. */ public function valid() { return !is_null($this->current()); } /** * Count elements of an object. Required by Countable interface. * * @link http://php.net/manual/en/countable.count.php * @return int The custom count as an integer. */ public function count() { return $this->_total; } /** * Returns an array of items for a page. Required by Zend_Paginator_Adapter_Interface. * * @param integer $offset Page offset * @param integer $itemCountPerPage Number of items per page * @return array */ public function getItems($offset, $itemCountPerPage) { $this->_notifyAccess($offset, $itemCountPerPage); $items = array(); for ($i = $offset; $i < ($offset + $itemCountPerPage); $i++) { $items[] = $this->_getRow($i); } return $items; } protected function _notifyAccess($offset, $length = 1) { // Empty on purpose. Child classes can extend to support lazy loading } }
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.
<?php /** * A "lazy" collection, which queries for row data as it is needed. * * @author Jason Grimes <jason@grimesit.com> * @link https://github.com/jasongrimes/zf-simple-datamapper * @category Jg * @package Mapper * @copyright Copyright (c) 2011 Jason Grimes <jason@grimesit.com> */ class Jg_Mapper_LazyCollection extends Jg_Mapper_Collection { /** * @var Zend_Db_Select */ protected $_select; /** * A Zend_Paginator_Adapter_DbSelect instance is used to automagically convert * the select object into a count query. @see count() * * @var Zend_Paginator_Adapter_DbSelect */ protected $_paginator_helper; public function __construct(Zend_Db_Select $select, Jg_Mapper $mapper) { $this->_select = $select; $this->_mapper = $mapper; $this->_paginator_helper = new Zend_Paginator_Adapter_DbSelect($select); } public function count() { if (is_null($this->_total)) { $this->_total = $this->_paginator_helper->count(); } return $this->_total; } public function getSelect() { return $this->_select; } protected function _notifyAccess($offset, $length = 1) { if (!$this->_issetRawSlice($offset, $length)) { $this->_loadRawSlice($offset, $length); } } protected function _issetRawSlice($offset, $length = 1) { $last_offset = $offset + $length - 1; if ($last_offset > $this->count()) { $last_offset = $this->count() - 1; } for ($i = $offset; $i <= $last_offset; $i++) { if (!isset($this->_raw[$i])) { return false; } } return true; } protected function _loadRawSlice($offset, $length = 1) { $select = $this->getSelect(); $select->limit($length, $offset); $data = $select->query()->fetchAll(); $i = $offset; foreach ($data as $row_data) { $this->_raw[$i] = $row_data; $i++; } } }
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.