Custom Pagination in Symfony 2

Featured Image © by ba1969/sxc.hu

NOTE: Tested on Symfony2 PR12. Might not work on later releases!
There is a newer version article about Symfony2 pagination that is compatible with Symfony2 – beta1.

Using Symfony 2, I noticed that there is no pagination included in framework (just some additional Bundle for download -and it uses Zend pagination but I don’t want to mix Zend into my Symfony 2 project), so I decided to write class for that on my own.This class is just in basic format (in the future will be added more features) but for basic purposes is usable.

Here is the case: I have view with list of items loaded from database and need pagination for that.

Basic idea:
Create ‘Paginator’ class and put all logic of pagination inside that class, and leave all database logic to Entity classes.

Here is the source code:

<?php
/**
 * Class to generate pagination for items
 *
 * @author Darko GoleΕ‘
 * @www.inchoo.net
 */
namespace Surgeworks\AdminBundle\Helpers;
 
class Paginator {
 
 //current displayed page
 protected $currentpage;
 //limit items on one page
 protected $limit;
 //total number of pages that will be generated
 protected $numpages;
 //total items loaded from database
 protected $itemscount;
 //starting item number to be shown on page
 protected $offset;
 
 function __construct($itemscount) {
 //set total items count from controller
 $this->itemscount = $itemscount;
 //get params from request URL
 $this->getParamsFromRequest();
 //Calculate number of pages total
 $this->getNumPages();
 //Calculate first shown item on current page
 $this->calculateOffset();
 }
 
 private function getParamsFromRequest() {
 //If current page number is set in URL
 if (isset($_GET['page'])) {
 $this->currentpage = $_GET['page'];
 } else {
 //else set default page to render
 $this->currentpage = 1;
 }
 //If limit is defined in URL
 if (isset($_GET['limit'])) {
 $this->limit = $_GET['limit'];
 } else {Β Β  //else set default limit to 20
 $this->limit = 10;
 }
 //If currentpage is set to null or is set to 0 or less
 //set it to default (1)
 if (($this->currentpage == null) || ($this->currentpage < 1)) {
 $this->currentpage = 1;
 }
 //if limit is set to null set it to default (10)
 if (($this->limit == null)) {
 $this->limit = 10;
 //if limit is any number less than 1 then set it to 0 for displaying
 //items without limit
 } else if ($this->limit < 1) {
 $this->limit = 0;
 }
 }
 
 private function getNumPages() {
 //If limit is set to 0 or set to number bigger then total items count
 //display all in one page
 if (($this->limit < 1) || ($this->limit > $this->itemscount)) {
 $this->numpages = 1;
 } else {
 //Calculate rest numbers from dividing operation so we can add one
 //more page for this items
 $restItemsNum = $this->itemscount % $this->limit;
 //if rest items > 0 then add one more page else just divide items
 //by limit
 $restItemsNum > 0 ? $this->numpages = intval($this->itemscount / $this->limit) + 1 : $this->numpages = intval($this->itemscount / $this->limit);
 }
 }
 
 private function calculateOffset() {
 //Calculet offset for items based on current page number
 $this->offset = ($this->currentpage - 1) * $this->limit;
 }
 
 //Returns HTML string with paginator elements - will be used from Controller
 public function RenderPaginator() {
 $html = '';
 //Insert all in one div tag
 $html.= '<div>';
 //We need this form for sumbitting limit into URL via GET call
 $html.='<form id= "paginator" name="paginator" method="get" action="#" >';
 //When limit is changed - just submit form
 $html.= '<select name="limit" onchange="javascript:document.forms.paginator.submit()">';
 $html.= '<option value="10" ';
 if ($this->limit == 10) {
 $html.='selected';
 } $html.='>10</option>';
 $html.= '<option value="20" ';
 if ($this->limit == 20) {
 $html.='selected';
 } $html.='>20</option>';
 $html.= '<option value="30" ';
 if ($this->limit == 30) {
 $html.='selected';
 } $html.='>30</option>';
 $html.= '<option value="50" ';
 if ($this->limit == 50) {
 $html.='selected';
 } $html.='>50</option>';
 $html.= '<option value="100" ';
 if ($this->limit == 100) {
 $html.='selected';
 } $html.='>100</option>';
 $html.= '<option value="500" ';
 if ($this->limit == 500) {
 $html.='selected';
 } $html.='>500</option>';
 $html.= '<option value="0" ';
 if ($this->limit == 0) {
 $html.='selected';
 } $html.='>All</option>';
 $html.='</select>';
 $html.='</form>';
 $html.='<ul>';
 
 //Generate links for pages
 for ($i = 1; $i < $this->numpages + 1; $i++) {
 
 $html.='<li><a href="' . $_SERVER['PHP_SELF'] . '?limit=' . $this->limit . '&amp;page=' . $i . '">' . $i . '</a></li>';
 }
 $html.='</ul>';
 $html.= '</div>';
 
 return $html;
 }
 
 //For using from controller
 public function getLimit() {
 return $this->limit;
 }
 //For using from controller
 public function getOffset() {
 return $this->offset;
 }
 
}

Now let’s look in database repository class:

<?php
namespace Surgeworks\AdminBundle\Entity;
 
use Doctrine\ORM\EntityRepository;
 
class LanguageRepository extends EntityRepository {
 
 public function getLanguagesListWithPagination($order_by = array(), $offset = 0, $limit = 0) {
 //Create query builder for languages table
 $qb = $this->createQueryBuilder('l');
 
 //Show all if offset and limit not set, also show all when limit is 0
 if ((isset($offset)) && (isset($limit))) {
 if ($limit > 0) {
 $qb->setFirstResult($offset);
 $qb->setMaxResults($limit);
 }
 //else we want to display all items on one page
 }
 //Adding defined sorting parameters from variable into query
 foreach ($order_by as $key => $value) {
 $qb->add('orderBy', 'l.' . $key . ' ' . $value);
 }
 //Get our query
 $q = $qb->getQuery();
 //Return result
 return $q->getResult();
 }
 
 public function getLanguagesCount() {
 //Create query builder for languages table
 $qb = $this->createQueryBuilder('l');
 //Add Count expression to query
 $qb->add('select', $qb->expr()->count('l'));
 //Get our query
 $q = $qb->getQuery();
 //Return number of items
 return $q->getSingleScalarResult();
 }
 
}

Now it’s time to use it in our controller action:

<?php
 
namespace Surgeworks\AdminBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\SecurityContext;
use Surgeworks\AdminBundle\Forms\LanguageForm;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Surgeworks\AdminBundle\Helpers\Paginator;
 
class LanguagesController extends Controller {
 
 /**
 * @extra:Route("/admin/languages", name="_admin_languages")
 * @extra:Template()
 */
 public function indexAction() {
 
 //order of items from database
 $order_by = array();
 //get Entity manager instance
 $em = $this->get('doctrine.orm.entity_manager');
 //get repository for class 'Language' : LanguageRepository.php
 $repository = $em->getRepository('Surgeworks\AdminBundle\Entity\Language');
 //get count of languages for using with Paginator class
 //Using custom made database query function in LanguageRepository class
 $languagesCount = $repository->getLanguagesCount();
 //When creating new paginator object it takes care for pages and items
 //organization based on numbers of items from database and limit variable in $_GET
 $paginator = new Paginator($languagesCount);
 //get returned HTML string from Paginator to render paginator HTML
 //elements in the template
 $strPaginator = $paginator->RenderPaginator();
 
 //If we have POST variable defined, than it is defined order of items
 //from inside form (clicking on sorting column for example)
 if ('POST' === $this->get('request')->getMethod()) {
 $order_by = array($_POST['filter_order'] => $_POST['filter_order_Dir']);
 $sort_direction = $_POST['filter_order_Dir'] == 'asc' ? 'desc' : 'asc';
 } else {
 //We know that nothing is changed for ordering columns -
 //this is alse default order of items when page is first opened
 $order_by = array('sort_order' => 'asc', 'id' => 'asc');
 $sort_direction = 'desc';
 }
 //To fill $languages for forwarding it to the template, we first call database function
 //with $offset and $limit to get items we wanted
 $languages = $repository->getLanguagesListWithPagination($order_by, $paginator->getOffset(), $paginator->getLimit());
 //Finally - return array to templating engine for displaying data.
 return array('languages' => $languages, 'sort_dir' => $sort_direction, 'paginator' => $strPaginator);
 }
...

Then in your twig template show it like that:

{{ paginator|raw }}

and maybe after all of this, add some CSS styles to make that paginator beautiful…


9 comments

  1. Thanks a lot for your post Darko, i had to make my own paginator for php and it took me a while, i wish i had seen this before haha.
    Good luck and keep on improving it!! πŸ™‚

  2. Imagine that I need to create a new class, written by me. Where should I put it in symfony2? So I could have acces to all of the methods.

  3. Thank you, Carlos for your good comments.
    I am sure that I will also modify my project with your improvements. You saved some of my time with that. Thanks! πŸ™‚

  4. Thanks for reply. In meantime I almost finished my paginator and because of that, It is too late for me to switch to some another, and also there is not need for that in my project. There is not much details more to show in this tutorial: it is just basic class for using in pagination without need to installing some additional ‘third party’ bundles. My paginator is not really intended to be uploaded to official bundles pages of Symfony2, but just for my internal use and this short tutorial is just to get idea to people who didn’t work with paginators before so I will be very happy if someone has more time to inprove it and reuse the code. Thanks for reading. πŸ™‚

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=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.