![]() |
|
Snippets |
|
This is just a version of the nonHydratingPager for Propel, adapted for Doctrine.
<?php /* Designed to be compatible with sfDoctrinePager only accept raw sql queries instead * of a Doctrine_Query object and spit back a resultset which you most likely want to * fill an array with instead of the standard array of hydrated objects. * * This solution is based on Propel's by Noel Tarnoff, Oz Basarir, dev AT (NOSPAM) naturalcapitalDOTorg * * @author David Morales, davidmoralesmojica AT (NOSPAM) gmailDOTcom * * Typical usage scenario: * 1) build 2 query strings with common WHERE clause, one for count one for selecting the rows * 2) pass the queries in with page and maxPerPage into the constructor ( no need to ->init() ) * 3) iterate through your result set (array of values) * 4) sit back and watch the fun * * ex. * $results = new nonHydratingPager($query_select, $query_count, $page, $max); * * foreach( $results as $result ) * { * ... * } */ class nonHydratingPager extends sfPager { private $resultSet = null; private $query = null; private $query_count = null; public function __construct($query, $query_count, $page = 1, $maxPerPage = 25) { $this->setPage($page); $this->setMaxPerPage($maxPerPage); $this->query = $query; $this->query_count = $query_count; } public function init() { $rs = Doctrine_Manager::getInstance()->getCurrentConnection()->fetchAssoc($this->query_count); $this->setNbResults($rs[0]['count']); $this->setLastPage(ceil($this->getNbResults() / $this->getMaxPerPage())); $startIndex = (($this->getPage()) - 1) * $this->getMaxPerPage(); $this->query .= ' LIMIT ' . $this->getMaxPerPage() . ' OFFSET ' . $startIndex; $this->resultSet = Doctrine_Manager::getInstance()->getCurrentConnection()->fetchAssoc($this->query); } public function getResults() { return $this->resultSet; } protected function retrieveObject($offset) {} }
<?php /* Designed to be compatible with sfPropelPager only accept raw sql queries instead * of a criteria object and spit back a resultset which you most likely want to * fill an array with instead of the standard array of hydrated objects. * * @author Noel Tarnoff, Oz Basarir dev AT (NOSPAM) naturalcapitalDOTorg * * Typical usage scenario: * 1) build 2 query strings with common WHERE clause, one for count one for selecting the rows * 2) pass the queries in with page and maxPerPage into the constructor ( no need to ->init() ) * 3) iterate through your result set and build your array of values * 4) sit back and watch the fun * * ex. * $objPager = new nonHydratingPager($query_select, $query_count, $page, $max); * * $rs = $objPager->getResultSet(); * * $arrEntities = array(); * while ( $rs->next() ) { * $arrEntities[$] = array('field1'=>$rs->getString(1), * 'field2'=>$rs->getString(2), * 'field3'=>$rs->getString(3)); * } */ class nonHydratingPager extends sfPager { private $resultSet = null; public function __construct($query, $query_count, $page = 1, $maxPerPage = 25) { $this->setPage($page); $this->setMaxPerPage($maxPerPage); $con = Propel::getConnection(); $stmt = $con->prepareStatement($query_count); $rs = $stmt->executeQuery(ResultSet::FETCHMODE_NUM); $rs->next(); $this->setNbResults($rs->get(1)); $this->setLastPage(ceil($this->getNbResults() / $this->getMaxPerPage())); $startIndex = (($this->getPage()) - 1) * $maxPerPage; $query .= ' LIMIT ' . $maxPerPage . ' OFFSET ' . $startIndex; $stmt = $con->prepareStatement($query); $this->resultSet = $stmt->executeQuery(ResultSet::FETCHMODE_NUM); } public function init() {} public function getResults() { return $this->resultSet; } protected function retrieveObject($offset) {} }
A common modification to a CRUD module is filtering records on the list page. There are probably several nice ways to do this, including drop-downs, forms, etc. depending on the situation. While some of these approaches are nice, they are often not minimal... We like minimal!
Say you have a book table, with a related author and genre. You want to filter the list of books by those related tables with links, paginate your list, and make it sortable via links in the header of the table.
To do this traditionally, you'll need lots of nasty little if statements in your template code, to check for all of these parameters (author_id, genre_id, title, num_pages, page, etc.) and combine them all together into a meaningful uri for your link_to function (something like "book/list?author_id=10&genre_id=5&sort=title&page=5"). We still want that link in the end, but generating it... How about a better way!
<!-- listSuccess.php --> <?php use_helpers('Filter', 'Pagination') ?> <h2>Books</h2> <h3>Filter By</h3> <table class="filters"> <tbody> <tr> <th>Author:</th> <td><?php echo filter_navigation(objects_for_filter($authors), 'author_id', $author_id) ?></td> </tr> <tr> <th>Genre:</th> <td><?php echo filter_navigation(objects_for_filter($genres), 'genre_id', $genre_id) ?></td> </tr> </tbody> </table> <hr /> <table class="list"> <thead> <tr> <th><?php link_to_unless($sort == 'id', 'Id', filter_url('sort', 'id')) ?></th> <th><?php link_to_unless($sort == 'title', 'Title', filter_url('sort', 'title')) ?></th> <th><?php link_to_unless($sort == 'author_id', 'Author', filter_url('sort', 'author_id')) ?></th> <th><?php link_to_unless($sort == 'genre_id', 'Genre', filter_url('sort', 'genre_id')) ?></th> </tr> </thead> <tbody> <?php foreach ($pager->getResults() as $book): ?> <tr> <td><?php echo $book->getId() ?></td> <td><?php echo $book->getTitle() ?></td> <td><?php echo $book->getAuthor()->getName() ?></td> <td><?php echo $book->getGenre()->getName() ?></td> </tr> <?php endforeach; ?> </tbody> </table> <?php echo pager_navigation($pager) ?>
NOTE: pager_navigation is covered at http://www.symfony-project.com/snippets/snippet/4, but needs to guess the uri, which is done at http://www.symfony-project.com/snippets/snippet/59.
NOTE: filter_navigation returns an unordered list, so you'll need css to display the <li> tags inline, and remove <ul> padding, margin, etc.
I'll leave it as an exercise to the reader to create the controller (action) code for this template, but it should be obvious.
<?php /** * Generate a url using the current internal uri, but replaces a param with a new value. * If the param is not in the current uri's query string, it is added instead. * * This is useful for a page that uses several filters to record sets, * and needs all the filters to work together, instead of blasting each * other away when a new link is clicked. * * <strong>Examples:</strong> * <code> * // with current uri => mymodule/myaction?author=10&genre=3 * * echo link_to('new author', filter_url('author', 5)); * // uri when clicked => mymodule/myaction?author=5&genre=3 * * echo link_to('new genre', filter_url('genre', 1)); * // uri when clicked => mymodule/myaction?author=10&genre=1 * * // with current uri => mymodule/myaction * * echo link_to('an author', filter_url('author', 10)); * // uri when clicked => mymodule/myaction?author=10 * </code> * * @param string the name of the parameter to replace * @param string the value to replace the current value with * @param boolean use route name * @return string the url with the parameter replaced * @see link_to */ function filter_url($param, $new_value, $with_route_name = false) { // fetch params from query string $params = _get_params(_get_query_string()); // replace param with new value $params[$param] = $new_value; return _get_uri($with_route_name) . '?' . _build_query_string($params); } /** * Removes a parameter from the current uri and returns the resulting url. * * @see filter_url */ function remove_filter_url($param, $with_route_name = false) { // fetch params from query string $params = _get_params(_get_query_string()); // remove param unset($params[$param]); return _get_uri($with_route_name) . '?' . _build_query_string($params); } /** * Generates an unordered list of links to filter the current record set by. * Multiple sets of filter_navigation links will work together, using the current uri. * * <strong>Examples:</strong> * <code> * echo filter_navigation(array(10=>'Jones', 12=>'Smith, J.', 13=>'Darby'), 'author_id', 13); * echo filter_navigation(objects_for_filter($authors), 'author_id', 13); * </code> * * @param array list of key=>value pairs of ids and strings * @param string the name of the parameter for this filter * @param string the selected id (or null, if none selected) * @param string the text to use for the "all" link * @see filter_url */ function filter_navigation($list, $param, $selected = null, $all_text = 'All') { $html = ''; $html .= content_tag('li', link_to_unless($selected === null, $all_text, remove_filter_url($param))); foreach ($list as $key => $value) { $html .= content_tag('li', link_to_unless($selected == $key, $value, filter_url($param, $key))); } return content_tag('ul', $html); } /** * Generates a simple list from a record set of propel objects. * Expects a getId function and a toString function. * * @param array objects to be converted to a list * @see filter_navigation */ function objects_for_filter($objects) { $list = array(); foreach ($objects as $object) { $list[$object->getId()] = $object->toString(); } return $list; } function _get_uri($with_route_name = false) { $internal_uri = sfRouting::getInstance()->getCurrentInternalUri($with_route_name); $ar = explode('?', $internal_uri); return ($with_route_name ? '@' : '') . $ar[0]; } function _get_query_string() { $internal_uri = sfRouting::getInstance()->getCurrentInternalUri(); $ar = explode('?', $internal_uri); return isset($ar[1]) ? $ar[1] : ''; } function _get_params($query_string) { // parse query string into associative array $params = array(); if ($query_string != '') { foreach (explode('&', $query_string) as $kvpair) { list($key, $value) = explode('=', $kvpair); $params[$key] = $value; } } return $params; } function _build_query_string($params) { // build list of key=value strings $ar = array(); foreach ($params as $key => $value) { $ar[] = $key . '=' . $value; } return implode('&', $ar); }
NOTE: Place this in apps/myapp/lib/helper/FilterHelper.php.
<?php echo link_to('new author', filter_url('author_id', 10)) ?> <?php echo url_for(filter_url('author_id', 10)) ?> <?php echo link_to_if($condition, 'new author', filter_url('author_id', 10)) ?> <?php echo link_to_unless($condition, 'new author', filter_url('author_id', 10)) ?> <?php echo button_to('new author', filter_url('author_id', 10)) ?>
<?php /** * Shortcut combining link_to and filter_url into single function. * * @see link_to * @see filter_url */ function link_to_filter($name, $param, $new_value, $options = array()) { return link_to($name, filter_url($param, $new_value), $options); } /** * Shortcut combining url_for and filter_url into single function. * * @see url_for * @see filter_url */ function filter_url_for($param, $new_value) { return url_for(filter_url($param, $new_value)); } /** * Shortcut combining link_to_if and filter_url into single function. * * @see link_to_if * @see filter_url */ function link_to_filter_if($condition, $name, $param, $new_value, $options = array()) { return link_to_if($condition, $name, filter_url($param, $new_value), $options); } /** * Shortcut combining link_to_unless and filter_url into single function. * * @see link_to_unless * @see filter_url */ function link_to_filter_unless($condition, $name, $param, $new_value, $options = array()) { return link_to_unless($condition, $name, filter_url($param, $new_value), $options); } /** * Shortcut combining button_to and filter_url into single function. * * @see button_to * @see filter_url */ function button_to_filter($name, $param, $new_value, $options = array()) { return button_to($name, filter_url($param, $new_value), $options); }
This class allows ajax request content to be easily paginated with desired visual effect.
<?php /** @name ajaxpager.class.php @desc allows ajax pagination. $pager:pager object created using sfPropelPager $url:URL of desired action.(mostly current ajax action) $divId:id of div to be updated after pagination $params:extra parameters to send with pager (but not implemented yet.you can implement it by yourself) $appear_effect:visual effect on completing the request.(default is 'Appear').You can use also 'Grow' or 'SlideDown' etc. @author Ahmet ERTEK, erteka@gmail.com @copyright DVS Bilisim, www.dvs-tr.com @version 1.0.0 */ class ajaxpager { private $pager; private $divId; private $url; private $params; private $appear_effect; /** @name ajaxpager.class.php @desc allows ajax pagination. $pager:pager object created using sfPropelPager $url:URL of desired action.(mostly current ajax action) $divId:id of div to be updated after pagination $params:extra parameters to send with pager (but not implemented yet.you can implement it by yourself) $appear_effect:visual effect on completing the request.(default is 'Appear').You can use also 'Grow' or 'SlideDown' etc. @author Ahmet ERTEK, erteka@gmail.com @copyright DVS Bilisim, www.dvs-tr.com @version 1.0.0 */ public function ajaxpager($pager,$url,$divId,$params=null,$appear_effect='Appear') { $this->pager=$pager; $this->divId=$divId; $this->url=$url; $this->params=$params; $this->appear_effect=$appear_effect; } /** @name ajaxpager.class.php @desc prints pagination. @author Ahmet ERTEK, erteka@gmail.com @copyright DVS Bilisim, www.dvs-tr.com @version 1.0.0 */ public function printPager() { $pager=$this->pager; $url=$this->url; $divId=$this->divId; $appear_effect=$this->appear_effect; if ($pager->haveToPaginate()) { echo link_to_remote('«', array( 'update' => $divId, 'url' => $url.'?page='.$pager->getFirstPage(), 'complete'=>visual_effect($appear_effect, $divId), 'loading'=>"$('$divId').innerHTML='<img src=/images/indicator.gif border=0>'", ), array('class'=>'contentLink')); link_to_remote('<', array( 'update' => $divId, 'url' => $url.'?page='.$pager->getPreviousPage(), 'complete'=>visual_effect($appear_effect, $divId), 'loading'=>"$('$divId').innerHTML='<img src=/images/indicator.gif border=0>'", ), array('class'=>'contentLink')); $links = $pager->getLinks(); foreach ($links as $page) { echo($page == $pager->getPage()) ? $page : link_to_remote($page, array( 'update' => $divId, 'url' => $url.'?page='.$page, 'complete'=>visual_effect($appear_effect,$divId), 'loading'=>"$('$divId').innerHTML='<img src=/images/indicator.gif border=0>'", ), array('class'=>'contentLink')); if ($page != $pager->getCurrentMaxLink()){ echo "-"; } } echo link_to_remote('»', array( 'update' => $divId, 'url' => $url.'?page='.$pager->getNextPage(), 'complete'=>visual_effect($appear_effect, $divId), 'loading'=>"$('$divId').innerHTML='<img src=/images/indicator.gif border=0>'", ), array('class'=>'contentLink')); link_to_remote('>', array( 'update' => $divId, 'url' => $url.'?page='.$pager->getLastPage(), 'complete'=>visual_effect($appear_effect, $divId), 'loading'=>"$('$divId').innerHTML='<img src=/images/indicator.gif border=0>'", ), array('class'=>'contentLink')); } } } ?>
Usage:
<?php $ajax_pager=new ajaxpager($pager,'myDivId','account/pictures?id='.$accountId,null,'SlideDown'); $ajax_pager->printPager(); ?>
I'm sure you've been there; you want to use Symfony's native sfPropelPager addon but you have to seed it with a complicated query that can't be reproduced via Criteria. In order to tackle this issue, I created a subclass of sfPropelPager called sfAdvancedPropelPager (it could probably benefit from a better name).
PLEASE NOTE: You will have to change all private methods and properties in sfPropelPager from 'private' to 'protected'. This won't break paging elsewhere in your projects, I promise you! What it will do is allow this subclass to gain access to it's parent's data and functionality. It should be noted that this addon was developed with the current stable version (0.6.3) of Symfony in mind.
I've cited the "search engine" example within the Askeet project as a suitable candidate for demonstration, albeit with some tweaks. See http://www.symfony-project.com/askeet/21 for details!!
<?php ... public static function search($phrase, $exact = false, $offset = 0, $max = 10) { $words = array_values(myTools::stemPhrase($phrase)); $nb_words = count($words); if (!$words) { return array(); } $con = sfContext::getInstance()->getDatabaseConnection('propel'); // define the base query $query = ' SELECT DISTINCT '.SearchIndexPeer::QUESTION_ID.', COUNT(*) AS nb, SUM('.SearchIndexPeer::WEIGHT.') AS total_weight FROM '.SearchIndexPeer::TABLE_NAME; if (sfConfig::get('app_permanent_tag')) { $query .= ' WHERE '; } else { $query .= ' LEFT JOIN '.QuestionTagPeer::TABLE_NAME.' ON '.QuestionTagPeer::QUESTION_ID.' = '.SearchIndexPeer::QUESTION_ID.' WHERE '.QuestionTagPeer::NORMALIZED_TAG.' = ? AND '; } $query .= ' ('.implode(' OR ', array_fill(0, $nb_words, SearchIndexPeer::WORD.' = ?')).') GROUP BY '.SearchIndexPeer::QUESTION_ID; // AND query? if ($exact) { $query .= ' HAVING nb = '.$nb_words; } $query .= ' ORDER BY nb DESC, total_weight DESC'; // prepare the statement $stmt = $con->prepareStatement($query); $placeholder_offset = 1; if (sfConfig::get('app_permanent_tag')) { $stmt->setString(1, sfConfig::get('app_permanent_tag')); $placeholder_offset = 2; } for ($i = 0; $i < $nb_words; $i++) { $stmt->setString($i + $placeholder_offset, $words[$i]); } // time to call our shiny new pager! $pager = new sfAdvancedPropelPager('Question', $max); $pager->setStatement($stmt); $pager->setPage(ceil($offset / $max)); $pager->init(); return $pager; } ... ?>
You would then assign the $pager to a suitable property in your action, and then handle your pagination in your template in the usual fashion as illustrated on <http://www.symfony-project.com/book/trunk/pager> - simple as that!
<?php class sfAdvancedPropelPager extends sfPropelPager { protected $statement = null; public function __construct($class, $defaultMaxPerPage = 10) { $this->setClass($class); $this->setMaxPerPage($defaultMaxPerPage); $this->setPage(1); $this->parameter_holder = new sfParameterHolder(); $this->setPeerMethod('retrieveByPk'); } public function init() { require_once(sfConfig::get('sf_model_lib_dir').'/'.$this->getClassPeer().'.php'); $rs = clone $this->getStatement()->executeQuery(ResultSet::FETCHMODE_NUM); $this->setNbResults($rs->getRecordCount()); if (($this->getPage() == 0 || $this->getMaxPerPage() == 0)) { $this->setLastPage(0); } else { $this->setLastPage(ceil($this->getNbResults() / $this->getMaxPerPage())); $this->statement->setOffset(($this->getPage() - 1) * $this->getMaxPerPage()); $this->statement->setLimit($this->getMaxPerPage()); } } public function getResults() { $rs = $this->getStatement()->executeQuery(ResultSet::FETCHMODE_NUM); $objects = array(); while ($rs->next()) { $objects[] = call_user_func(array($this->getClassPeer(), $this->getPeerMethod()), $rs->getInt(1)); } return $objects; } protected function retrieveObject($offset) { $statement = clone $this->getStatement(); $statement->setOffset($offset - 1); $statement->setLimit(1); $rs = $statement->executeQuery(ResultSet::FETCHMODE_NUM); $object = null; while ($rs->next()) { $object = call_user_func(array($this->getClassPeer(), $this->getPeerMethod()), $rs->getInt(1)); } return $object; } public function getStatement() { return $this->statement; } public function setStatement($stmt) { $this->statement = $stmt; } } ?>
sfPropelPager allow you to use sufferless pagination but it only works for Objects. Sometimes, you can't get you array of Object using a Criteria. Here is a class that you can use exactly as the sfPropelPager but witch works using an array of objects, not a Criteria or peer classes or even a custom SQL (see timu EREN's snipeet). It has only a small differences with the original sfPropelPager.
You use it exactly the same way as you would with the sfPropelPager object.
Here is a small example:
$Ps = $category->getProducts(); // here you can use any kind of array of objects $pager = new myTabPager($Ps, 15); $pager->setPage($this->getRequestParameter('page', 1)); $pager->init(); $this->pager = $pager;
<?php /* * This file is an addon to the symfony package */ /** * @package symfony * @subpackage addon * @author Fabien Potentier modified by Karl */ /** * * myTabPager class. * * @package symfony * @subpackage addon * @author Fabien Potentier modified by Karl */ class myTabPager { private $page = 1, $tab = array(), $maxPerPage = 0, $lastPage = 1, $nbResults = 0, $objects = null, $cursor = 1, $parameters = array(), $currentMaxLink = 1, $parameter_holder = null; public function __construct($tab, $defaultMaxPerPage = 10) { $this->setTab($tab); $this->setMaxPerPage($defaultMaxPerPage); $this->setPage(1); $this->parameter_holder = new sfParameterHolder(); } public function init() { $this->setNbResults(count($this->tab)); if (($this->getPage() == 0 || $this->getMaxPerPage() == 0)) { $this->setLastPage(0); } else { $this->setLastPage(ceil($this->getNbResults() / $this->getMaxPerPage())); } } public function setTab($tab) { $this->tab = $tab; } public function getTab() { return $this->tab; } public function getCurrentMaxLink() { return $this->currentMaxLink; } public function getLinks($nb_links = 5) { $links = array(); $tmp = $this->page - floor($nb_links / 2); $check = $this->lastPage - $nb_links + 1; $limit = ($check > 0) ? $check : 1; $begin = ($tmp > 0) ? (($tmp > $limit) ? $limit : $tmp) : 1; $i = $begin; while (($i < $begin + $nb_links) && ($i <= $this->lastPage)) { $links[] = $i++; } $this->currentMaxLink = $links[count($links) - 1]; return $links; } public function haveToPaginate() { return (($this->getPage() != 0) && ($this->getNbResults() > $this->getMaxPerPage())); } public function getCursor() { return $this->cursor; } public function setCursor($pos) { if ($pos < 1) { $this->cursor = 1; } else if ($pos > $this->nbResults) { $this->cursor = $this->nbResults; } else { $this->cursor = $pos; } } public function getObjectByCursor($pos) { $this->setCursor($pos); return $this->getCurrent(); } public function getCurrent() { return $this->retrieveObject($this->cursor); } public function getNext() { if (($this->cursor + 1) > $this->nbResults) { return null; } else { return $this->retrieveObject($this->cursor + 1); } } public function getPrevious() { if (($this->cursor - 1) < 1) { return null; } else { return $this->retrieveObject($this->cursor - 1); } } private function retrieveObject($offset) { return $this->tab[$offset]; } public function getResults() { return array_slice($this->tab,($this->getPage() - 1) * $this->getMaxPerPage(),$this->maxPerPage); } public function getFirstIndice() { if ($this->page == 0) { return 1; } else { return ($this->page - 1) * $this->maxPerPage + 1; } } public function getLastIndice() { if ($this->page == 0) { return $this->nbResults; } else { if (($this->page * $this->maxPerPage) >= $this->nbResults) { return $this->nbResults; } else { return ($this->page * $this->maxPerPage); } } } public function getNbResults() { return $this->nbResults; } private function setNbResults($nb) { $this->nbResults = $nb; } public function getFirstPage() { return 1; } public function getLastPage() { return $this->lastPage; } private function setLastPage($page) { $this->lastPage = $page; if ($this->getPage() > $page) { $this->setPage($page); } } public function getPage() { return $this->page; } public function getNextPage() { return min($this->getPage() + 1, $this->getLastPage()); } public function getPreviousPage() { return max($this->getPage() - 1, $this->getFirstPage()); } public function setPage($page) { $page = intval($page); $this->page = ($page <= 0) ? 1 : $page; } public function getMaxPerPage() { return $this->maxPerPage; } public function setMaxPerPage($max) { if ($max > 0) { $this->maxPerPage = $max; if ($this->page == 0) { $this->page = 1; } } else if ($max == 0) { $this->maxPerPage = 0; $this->page = 0; } else { $this->maxPerPage = 1; if ($this->page == 0) { $this->page = 1; } } } public function getParameterHolder() { return $this->parameter_holder; } public function getParameter($name, $default = null, $ns = null) { return $this->parameter_holder->get($name, $default, $ns); } public function hasParameter($name, $ns = null) { return $this->parameter_holder->has($name, $ns); } public function setParameter($name, $value, $ns = null) { return $this->parameter_holder->set($name, $value, $ns); } } ?>
I needed a one pager for custom SQL and i read the sfPropelPager and i change like this.
Howto use: write two SQL in Peer class like this.
public function getReportsWithPager($params) { $WHERE = "FROM ".self::TABLE_NAME." WHERE ".self::SOMEWHEREFIELD." = '%s'"; $SQL ="SELECT SUM(".self::FIELDNAME."), ".self::WHOIS." %s"; $cSQL = "SELECT COUNT(id) %s" $WHERE = sprintf($WHERE, $params['where']); $SQL = sprintf($SQL, $WHERE); $cSQL = sprintf($cSQL, $WHERE); $pager = new myCustomPager($cSQL, $SQL, $params['maxpage']); $pager->setPage($params['page']); $pager->init(); return $pager; }
And call in your action:
$this->pager = SomePeer::getReportsWithPager($params);
in Template,
<?php $list = $pager->getResults(); while($list->next()): ?> <div><?php echo $list->get('sum'); ?></div> <?php endwhile; ?>
And creation page links same with sfPropelPager, read the sfPropelPager.
<?php /* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * */ /** * @package symfony * @subpackage addon * @author Fabien Potencier <fabien.potencier@symfony-project.com> * @version SVN: $Id: sfPropelPager.class.php 1415 2006-06-11 08:33:51Z fabien $ */ /** * * myCustomPager class. * * @package symfony * @subpackage addon * @author Fabien Potencier <fabien.potencier@symfony-project.com> * * Changed: sfPropelPager.class.php * Changed by: Timu EREN * Last Changed: 12 / 08 /2006(dd /mm/ YYYY) */ class myCustomPager { private $page = 1, $maxPerPage = 0, $lastPage = 1, $nbResults = 0, $query = null, $countQuery = null, $objects = null, $cursor = 1, $parameters = array(), $currentMaxLink = 1; public function __construct($cSQL,$SQL, $defaultMaxPerPage = 10) { $con = Propel::getConnection(); $SQL = $con->prepareStatement($SQL); $cSQL = $con->prepareStatement($cSQL); $this->setQuery($SQL); $this->setCountQuery($cSQL); $this->setMaxPerPage($defaultMaxPerPage); $this->setPage(1); } public function init() { $cForCount = $this->getCountQuery(); $c = $cForCount->executeQuery(); $c->next(); $this->setNbResults($c->get('count')); $c = $this->getQuery(); $c->setLimit(0); $c->setOffset(0); if (($this->getPage() == 0 || $this->getMaxPerPage() == 0)) { $this->setLastPage(0); } else { $this->setLastPage(ceil($this->getNbResults() / $this->getMaxPerPage())); $c->setOffset(($this->getPage() - 1) * $this->getMaxPerPage()); $c->setLimit($this->getMaxPerPage()); } } public function getCurrentMaxLink() { return $this->currentMaxLink; } public function getLinks($nb_links = 5) { $links = array(); $tmp = $this->page - floor($nb_links / 2); $check = $this->lastPage - $nb_links + 1; $limit = ($check > 0) ? $check : 1; $begin = ($tmp > 0) ? (($tmp > $limit) ? $limit : $tmp) : 1; $i = $begin; while (($i < $begin + $nb_links) && ($i <= $this->lastPage)) { $links[] = $i++; } $this->currentMaxLink = $links[count($links) - 1]; return $links; } public function haveToPaginate() { return (($this->getPage() != 0) && ($this->getNbResults() > $this->getMaxPerPage())); } public function getCursor() { return $this->cursor; } public function setCursor($pos) { if ($pos < 1) { $this->cursor = 1; } else if ($pos > $this->nbResults) { $this->cursor = $this->nbResults; } else { $this->cursor = $pos; } } public function getObjectByCursor($pos) { $this->setCursor($pos); return $this->getCurrent(); } public function getCurrent() { return $this->retrieveObject($this->cursor); } public function getNext() { if (($this->cursor + 1) > $this->nbResults) { return null; } else { return $this->retrieveObject($this->cursor + 1); } } public function getPrevious() { if (($this->cursor - 1) < 1) { return null; } else { return $this->retrieveObject($this->cursor - 1); } } private function retrieveObject($offset) { $cForRetrieve = $this->getQuery(); $cForRetrieve->setOffset($offset - 1); $cForRetrieve->setLimit(1); $results = $cForRetrieve->executeQuery(); return $results; } public function getResults() { $c = $this->getQuery(); return $c->executeQuery(); } public function getFirstIndice() { if ($this->page == 0) { return 1; } else { return ($this->page - 1) * $this->maxPerPage + 1; } } public function getLastIndice() { if ($this->page == 0) { return $this->nbResults; } else { if (($this->page * $this->maxPerPage) >= $this->nbResults) { return $this->nbResults; } else { return ($this->page * $this->maxPerPage); } } } public function getQuery() { return $this->query; } public function setQuery($c) { $this->query = $c; } public function getCountQuery() { return $this->countQuery; } public function setCountQuery($c) { $this->countQuery = $c; } public function getNbResults() { return $this->nbResults; } private function setNbResults($nb) { $this->nbResults = $nb; } public function getFirstPage() { return 1; } public function getLastPage() { return $this->lastPage; } private function setLastPage($page) { $this->lastPage = $page; if ($this->getPage() > $page) { $this->setPage($page); } } public function getPage() { return $this->page; } public function getNextPage() { return min($this->getPage() + 1, $this->getLastPage()); } public function getPreviousPage() { return max($this->getPage() - 1, $this->getFirstPage()); } public function setPage($page) { $page = intval($page); $this->page = ($page <= 0) ? 1 : $page; } public function getMaxPerPage() { return $this->maxPerPage; } public function setMaxPerPage($max) { if ($max > 0) { $this->maxPerPage = $max; if ($this->page == 0) { $this->page = 1; } } else if ($max == 0) { $this->maxPerPage = 0; $this->page = 0; } else { $this->maxPerPage = 1; if ($this->page == 0) { $this->page = 1; } } } } ?>
This is helper is mostly inspired by Pagination navigation helper, but there are some differences:
function pagination($pager) { $uri = sfRouting :: getInstance()->getCurrentInternalUri(); $html = ''; if ($pager->haveToPaginate()) { $uri .= strstr($uri, '?') ? '&page=' : '?page='; if ($pager->getPage() != 1) { $html .= '<li>' . link_to('first', $uri . '1') . '</li>'; $html .= '<li>' . link_to('previous', $uri . $pager->getPreviousPage()) . '</li>'; } foreach ($pager->getLinks() as $page) { if ($page == $pager->getPage()) $html .= '<li><strong>' . link_to($page, $uri . $page) . '</strong></li>'; else $html .= '<li>' . link_to($page, $uri . $page) . '</li>'; } if ($pager->getPage() != $pager->getLastPage()) { $html .= '<li>' . link_to('next', $uri . $pager->getNextPage()) . '</li>'; $html .= '<li>' . link_to('last', $uri . $pager->getLastPage()) . '</li>'; } $html = '<ul class="pagination">' . $html . '</ul>'; } return $html; }
ul.pagination li { display: inline; list-style-type: none; padding-right: 1em; }
<?php echo use_helper('Pagination') ?> <?php echo pagination($pager) ?>
Suppose you have a list of items (products, contacts, etc) that you want to look up by name. You want them paginated by letter of the alphabet, not by a certain number of items on each page (the default pagination behavior). You will have a navigator that includes every letter of the alphabet, but only the letters that actually have items will be links. The current letter you are looking at will be highlighted. This is similar to a phone book.
The drawback to using this method is that the entire contents of the table have to be loaded for each page in order to build the array of first letters for the navigator. Perhaps there would be a way to query the database for just the DISTINCT first letters of the LNAME field, in effect: SELECT DISTINCT substr(lname,1,1) FROM phonebook. If anyone knows how to go about that, please comment.
First, create your action in action.class.php:
public function executeList() { $c = new Criteria(); $c->addAscendingOrderByColumn(PhoneBook::LNAME); $entries = PhoneBook::doSelect($c); //now we need an array of all the first letters. //At the same time we're looping, we'll put all the //items we need for this page into a separate array $pageLetter = strtoupper($this->getRequestParameter('page','A')); $arrayOfFirstLetters = array(); $thisList = array(); $x=0;$y=0; foreach($entries as $entry) { $firstLetter = strtoupper(substr($entry->getLName(),0,1)); $arrayOfFirstLetters[$x++]=$firstLetter; if($pageLetter==$firstLetter) { $thisList[$y++] = $entry; } } $arrayOfFirstLetters = array_unique($arrayOfFirstLetters); sort($arrayOfFirstLetters); //make the arrays available to the page template $this->page = $pageLetter; $this->pageLinks = $arrayOfFirstLetters; $this->names = $thisList; }
And the template, listSuccess.php:
<?php include_partial('alphaPager',array('letters'=>$pageLinks,'thisPage'=>$page)) ?> <table> <?php foreach ($names as $name): ?> <tr> <td> <?php echo link_to($name->getLName().', '.$name->getFName(), 'phonebook/show?id='.$name->getId()) ?> </td> </tr> <?php endforeach; ?> </table>
And now the partial template, _alphaPager.php:
<div id="alphapager"> <?php $x='A'; for($y=0;$y++<26;$x++) { if($y>1) { echo ' | '; } if($x==$thisPage) { echo '<font class="alhpaHL">'.$x.'</font>'; } else { if(array_search($x,$letters)) { echo link_to($x,'phonebook/list?page='.$x); } else { echo $x; } } } ?> </div>
Let us take the example of snipeet! :-) The function that selects snippets by tags is a custom SQL query. When filtering by tags there is no pager anymore.
Why is that? The problem is that sfPropelPaginate only works with a Criteria object, not with a raw query. As a result you cannot have pagination with raw SQL query which is a rather severe limitation.
The solution is to first encode the parameter of the query in the criteria (that's the tricky bit) and then to set up a custom peer method.
As i said earlier we will take a simplified version of snipeet as an example. We assume that we have a function that creates a sql statement from a list of tags.
Here we go about setting the dummy criteria that encodes an array of tags:
getCriteriaFromTags($tags) { $c = new Criteria(); for ($i = 1; $i <= count($tags); ++$i) { $prefix = 't'.$i; // note that that SnippetTagPeer::TABLE_NAME SnippetTagPeer::NAME are in fact arbitrary // any other table and table.column combination would work as well $c->addAlias($prefix.SnippetTagPeer::TABLE_NAME, SnippetTagPeer::TABLE_NAME); $c->add($prefix.SnippetTagPeer::NAME, $tags[$i-1]); } return $c; }
Now you can initialise a sfPropelPaginate object as usual with that Criteria object. When that is done you tell it to use your custom peer method:
$pager->setPeerMethod('doSelectByTags');
Next you code the doSelectByTags method that performs the actual selection from a criteria object:
function doSelectByTags($c) { // first you fetch the tags from the query $tags = array(); foreach($c->keys() as $key) $tags[] = $c->get($key); // here comes your custom sql query // it basically creates a $statement variable from the $tags array // ..... // now you have to add the limit and offset: $statement->setLimit($c->getLimit()); $statement->setOffset($c->getOffset()); // and the rest of the code is as usual }
In a template displaying a paginated list, you need to show the pager navigation. Create a PaginationHelper.php in lib/helper:
<?php function pager_navigation($pager, $uri) { $navigation = ''; if ($pager->haveToPaginate()) { $uri .= (preg_match('/\?/', $uri) ? '&' : '?').'page='; // First and previous page if ($pager->getPage() != 1) { $navigation .= link_to(image_tag('/sf/images/sf_admin/first.png', 'align=absmiddle'), $uri.'1'); $navigation .= link_to(image_tag('/sf/images/sf_admin/previous.png', 'align=absmiddle'), $uri.$pager->getPreviousPage()).' '; } // Pages one by one $links = array(); foreach ($pager->getLinks() as $page) { $links[] = link_to_unless($page == $pager->getPage(), $page, $uri.$page); } $navigation .= join(' ', $links); // Next and last page if ($pager->getPage() != $pager->getLastPage()) { $navigation .= ' '.link_to(image_tag('/sf/images/sf_admin/next.png', 'align=absmiddle'), $uri.$pager->getNextPage()); $navigation .= link_to(image_tag('/sf/images/sf_admin/last.png', 'align=absmiddle'), $uri.$pager->getLastPage()); } } return $navigation; }
In your templates, display the pagination links like that:
<?php echo use_helper('Pagination') ?> <?php echo pager_navigation($mypager, '@myrule') ?>