KNP paginator reborn

Published on

Nov 24, 2011

technical

Nov 25, 2011 − A different, simple, extensible pagination tool for Symfony2 and other projects. Magic? no! only KISS principle Why reinventing the wheel? Can someone tell me, what is the definition of wheel in software world?

Introduction

Knp Paginator first was introduced as a Bundle for Sf2 projects and was using Zend as the main dependency to handle pagination. Recently we have split it into a Pager Component without any hard dependencies except Symfony EventDispatcher Component and of course >= PHP 5.3

In this article we will be talking about the Symfony2 KnpPaginatorBundle which is in general based on the knp pager component

How is it different? First of all, it uses event dispatcher to paginate whatever is needed. Pagination process involves triggering events which hits the subscribers and if the subscriber knows how to paginate the given object it paginates. Further more, some subscriber must initialize the pagination view object, which will be the result of pagination request. Pagination view can be anything which will be responsible on how to render the pagination. Finally, it is already known of capability to automate the sorting of pagination data, which also brings variety of extension points, like: filtering, searching

So how does it look on the real sf2 controller

When trying to paginate an array?

$paginator = $this->get('knp_paginator'); //(1)
$target = range('a', 'u'); //(2) array('a', 'b', ... 'u');
// uses event subscribers to paginate $target
$slice = $paginator->paginate($target, 2/*page*/, 10/*limit*/); //(3)
// $slice is a pagination view, represents the paginated data

Pagination of Doctrine query:

$paginator = $this->get('knp_paginator'); // (1)
$query = $doctrineEntityManager->createQuery('SELECT a FROM EntityArticle a'); //(2)
$articles = $paginator->paginate($query, 2/*page*/, 10/*limit*/); // (3)
// $articles is instance of SlidingPagination or any custom one

What is happening behind the scene

  1. Get instance of paginator, it is constructed and based with EventDispatcher which has different kinds of event subscribers hooked. Mostly ones that count and paginate the items for the given object, if it supports it.
  2. Initialize the target to be paginated, depends on what paginator subscribers support.
  3. Paginate method uses the dispatcher and triggers events for: count, items and pagination view. After all this information is aquired by listeners pagination view is being built.

Pagination view (template)

In the given context, $articles is a pagination view class instance, which is populated, with actual pagination data, by paginator.

By default it creates Sliding pagination instance, it can be overrided by a higher priority event listener. In general this class should be immutable and responsible only for rendering or accessing the pagination data.

So how it looks like in the twig template:

<table>
<tr>
{# sorting of properties based on query components: a - EntityArticle #}
    <th>{{ articles.sortable('Id', 'a.id')|raw }}</th>
    <th>{{ articles.sortable('Title', 'a.title')|raw }}</th>
</tr>

{# table body #}
{% for article in articles %}
<tr {% if loop.index is odd %}class="color"{% endif %}>
    <td>{{ article.id }}</td>
    <td>{{ article.title }}</td>
</tr>
{% endfor %}
</table>
{# display navigation #}
<div class="navigation">
    {{ articles.render()|raw }}
</div>

As mentioned before articles is a SlidingPagination view instance. Sortable method creates a sort link which also handles the sort direction and doctrine query manipulation behind the scene.

render method loads and renders the default sliding pagination template, it can be overrided through parameter or configuration. More documentation will be available on the bundle itself when it is stable.

Creating custom subscriber

Lets say we want to paginate a directory content, might be quite interesting. And when we have such a handy Finder component in symfony, its easily achievable.

I will asume we you just installed symfony-standard edition and you install KnpPaginatorBundle. Follow the installation guide on these repositories, its very easy to setup.

Next, lets extend our AcmeDemoBundle which comes together with symfony-standard edition. Create file ../symfony-standard/src/Acme/DemoBundle/Subscriber/PaginateDirectorySubscriber.php

<?php

// file: ../symfony-standard/src/Acme/DemoBundle/Subscriber/PaginateDirectorySubscriber.php
// requires // SymfonyComponentFinderFinder

namespace AcmeDemoBundleSubscriber;

use SymfonyComponentFinderFinder;
use SymfonyComponentEventDispatcherEventSubscriberInterface;
use KnpComponentPagerEventCountEvent;
use KnpComponentPagerEventItemsEvent;

class PaginateDirectorySubscriber implements EventSubscriberInterface
{
    private $files;

    public function count(CountEvent $event)
    {
        $dir = $event->getTarget();
        if (is_dir($dir)) {
            $finder = new Finder;
            $finder
                ->files()
                ->depth('< 4') // 3 levels
                ->in($dir)
            ;
            $iter = $finder->getIterator();
            $this->files = iterator_to_array($iter);
            $event->setCount(count($this->files));
            $event->stopPropagation();
        }
    }

    public function items(ItemsEvent $event)
    {
        $dir = $event->getTarget();
        if (is_dir($dir)) {
            $event->setItems(array_slice(
                $this->files,
                $event->getOffset(),
                $event->getLimit()
            ));
            $event->stopPropagation();
        }
    }

    public static function getSubscribedEvents()
    {
        return array(
            'items' => array('items', 1/*increased priority to override any internal*/),
            'count' => array('count', 1/*increased priority*/)
        );
    }
}

Class above is the simple event subscriber, which listens to count and items events. Creates a finder and looks in this directory for files. To be more specific it will look for the files in the directory being paginated, max in 3 level depth.

Next we need to tell knp_paginator about our new fancy subscriber which we intend to use in pagination. It is also very simple, create aditional service config file: ../symfony-standard/src/Acme/DemoBundle/Resources/config/paginate.xml

<?xml version="1.0" ?>

<!-- file: ../symfony-standard/src/Acme/DemoBundle/Resources/config/paginate.xml -->

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="acme.directory.subscriber" class="AcmeDemoBundleSubscriberPaginateDirectorySubscriber" scope="request">
            <tag name="knp_paginator.subscriber" />
        </service>
    </services>
</container>

Now to finish this configuration we need to load it from our dependency injection extension. Modify file: ../symfony-standard/src/Acme/DemoBundle/DependencyInjection/config/AcmeDemoExtension.php

// modify load method, to look like:
public function load(array $configs, ContainerBuilder $container)
{
    $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
    $loader->load('services.xml');
    // loading our pagination services
    $loader->load('paginate.xml');
}

Finally, we are done with configuration, now lets create actual controller action. Modify controller: ../symfony-standard/src/Acme/DemoBundle/Controller/DemoController.php And add the following action, which paginates the previous directory

/**
 * @Route("/test", name="_demo_test")
 * @Template()
 */
public function testAction()
{
    $paginator = $this->get('knp_paginator');
    $files = $paginator->paginate(
        __DIR__.'/../',
        $this->get('request')->query->get('page', 1),
        10
    );
    return compact('files');
}

And the last thing is the template, create: ../symfony-standard/src/Acme/DemoBundle/Resources/views/Demo/test.html.twig

{% extends "AcmeDemoBundle::layout.html.twig" %}

{% block title "Symfony - Demos" %}

{% block content_header '' %}

{% block content %}
    <h1>Available demos</h1>
    <ul id="demo-list">
        <li><a href="{{ path('_demo_hello', {'name': 'World'}) }}">Hello World</a></li>
        <li><a href="{{ path('_demo_secured_hello', {'name': 'World'}) }}">Access the secured area</a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="{{ path('_demo_login') }}">Go to the login page</a></li>
        {# <li><a href="{{ path('_demo_contact') }}">Send a Message</a></li> #}
    </ul>

    <table>
    <tr>
    {# sorting of properties based on query components #}
        <th>base name</th>
        <th>path</th>
    </tr>

    {# table body #}
    {% for file in files %}
    <tr {% if loop.index is odd %}class="color"{% endif %}>
        <td>{{ file.getBaseName() }}</td>
        <td>{{ file.getPath() }}</td>
    </tr>
    {% endfor %}
    </table>
    {# display navigation #}
    <div id="navigation">
        {{ files.render()|raw }}
    </div>
{% endblock %}

Do not forget to reload the cache: ./app/console ca:c -e dev You should find some files paginated if you open the url: http://baseurl/app_dev.php/demo/test

Future plans

  • Extend core subscribers to support Doctrine query builders.
  • Improve pagination and paginator communication.
  • Require whitelist for sorting listeners
  • Support endless pagination like tweets through API

Enjoy and contribute

Do not forget - sharing is caring :) and we are always happy to hear you out.

KnpPaginatorBundle by Knplabs. Follow me on twitter and github

Written by

KNP Labs
KNP Labs

Comments