KNP paginator reborn

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

2 thoughts on “KNP paginator reborn

  1. Hello.
    I’ve got a problem with friendly url in KNP – how to do friendly URL with KPN in Symfony2 ? Like “/products/page/2″ or “/products/page/2/sort/id/asc”
    Thanks

    1. Hello!

      First thanks for the interest you give to this :)

      I don’t think page number and sorting should be part of route **attributes**, they are very well placed as url *query params*.

      But if you really want them as route attributes, then you must put placeholders in your route’s url pattern:

      pattern: /products/page/{page}/sort/{property}/{order}

      Then you’ll have to implement your own subscriber to get informations from the Request’s attributes instead of the $_GET array.

      Take example from https://github.com/KnpLabs/knp-components/blob/master/src/Knp/Component/Pager/Event/Subscriber/Sortable/Doctrine/ORM/QuerySubscriber.php#L11

      Hope it helps!
      Cheers.

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>