whateverthing.com

Pagination. We meet again.

Recently I had a side project that needed Next/Previous pagination buttons. Using Twig, a PHP-based template rendering engine I like, I was able to hack together a quick and dirty solution. Emphasis on the dirty - but, even dirty code can teach you something.

In this case, we're going to learn a few Twig tricks: advanced for loops, array merging, and custom macros.

My main data listing used a for/in loop to show all results. The dataset was fairly small, so I wasn’t planning on altering the database query to limit the results retrieved (especially since I was planning to cache as much as possible). Everything was sent straight to Twig.

The first problem I needed to solve was limiting the for loop to a subset of the array. In PHP, one solution is to use the continue statement to skip items that didn’t need to be processed. An interesting thing about Twig is that there isn’t an equivalent to this - so, I had to find a Twig-friendly solution. A quick bit of RTFM showed that Twig’s slice command would act like PHP’s array_slice(), allowing me to limit the loop to the exact subset I needed. I also found that I could use the length command to check the total number of elements in the array.

Showing only the first ten elements of an array in Twig would look something like {% for item in items[0:10] %}, where the square bracket notation is a shorthand syntax for slice. The first number is the starting position and the second number is the length. Twig is forgiving about whether the length matches the length of the array or not, which allows our logic to be quite clean (no need to clutter the code with length rules).

The next problem was that I needed to dress up my output. In my case I wanted to add a table header/footer, but for the sake of this post I’ll just demonstrate with an unordered list. Twig provides a few useful loop shorthand values, and we’ll need to use two of them - loop.first and loop.last:

{% for item in items[0:10] %}
    {% if loop.first %}<ul>{% endif %}
    <li>{{ item.name }}</li>
    {% if loop.last %}</ul>{%endif %}
{% endfor %}

Now for the fun part - the actual buttons. Twig’s macro functionality will make it easy to reuse this code in other places, so that’s nice. I'm going to want to add a special parameter to the route, I've chosen to do it here by merging {'_index': current-limit} (current minus limit) into the extras array using the + operator. Here’s the macro I ended up with:

{% macro paginate(current, limit, total, route, extras = {}) %}
  {% if total > limit %}
      <a
       href="{{app.url_generator.generate(route, extras + {'_index': current - limit})}}"
       class="btn btn-info {% if current - 1 < 1 %}disabled{% endif %}">
          &laquo; Previous Page
      </a>
      <a
       href="{{app.url_generator.generate(route, extras + {'_index': current+limit})}}"
       class="btn btn-info {% if current + 1 > (total - limit) %}disabled{% endif %}">
          Next Page &raquo;
      </a>
  {% endif %}
{% endmacro %}

It’s kind of an ugly mess, so a quick run-through of the usage is in order:

  • To invoke the macro, first you save it to a twig file and then include it in your template:
    • {% import "my_macros.html.twig" as gt %}
  • Then you call it with the appropriate parameters:
    • current: The reference point for the current page. Essentially, “current page * number of items per page”.
    • limit: The number of items per page.
    • total: The total number of items in the list.
    • route: The name of the current route (to be passed to the URL generator).
    • extras: Extra parameters for the current route.
  • Example:
    • {{ gt.paginate(0, 10, 100, ‘home’) }}

Note that I left off the “extras” parameter in the example. It’s only really necessary when extra parameters are needed for the route, such as saving a keyword string in a search query. In this case it will default to an empty array.

Once invoked, the logic is as follows:

  • If there are more items than can be shown on one page, we want to output our buttons.
  • Render the Previous button using Twitter Bootstrap and the Silex URL Generator
    • If the current reference point (less one) is less than one - in other words, if we’re on the first page - flag the Previous button as disabled
    • Otherwise, set the Previous button URL to the specified route and give it the _index value of “current” minus “limit” - the previous page.
  • Render the Next button
    • If the current reference point (plus one) is greater than total minus limit - the last page - then disable the Next button
    • Otherwise, set the Next button URL to the specified route and give it the _index value of “current” plus “limit” - the next page

Because I was using Twitter Bootstrap for my CSS, I added a “disabled” class to make the buttons unclickable - if you’re using some other CSS for buttons, you’ll probably have to either hide the buttons or use some other method to indicate they can’t be clicked.

The key to tying all of this together is being able to retrieve the current reference point. You can do this in PHP and sanitize it before sending it to Twig in the ->render statement (Note that Silex users can play fast and loose by using something like “app.request._index” - but be careful, that parameter might be swarthy).

The final version of the for loop would look like this:

{% for item in items[PageOffset|default(0):PageLimit] %}
    {% if loop.first %}<ul>{% endif %}
    <li>{{ item.name }}</li>
    {% if loop.last %}
        </ul>
        {{gt.paginate(
            PageOffset|default(0),
            PageLimit,
            items|length,
            app.request.attributes.get('_route’),
            app.request.attributes.get('_route_params')
        )}}
    {%endif %}
{% else %}
    <p><strong>No results.</strong></p>
{% endfor %}

It’s not pretty, but it gets the job done. Note that I've also used Twig's "else" construct in case there are no elements in the items array. This is one construct that PHP itself should consider including.

That's all for now. Thanks for checking in!

Published: May 13, 2014

Categories: coding, howto

Tags: dev, development, coding, silex, howto, bootstrap, twig