Everything You Always Wanted to Know About Grouping
When working with data in templates, you often encounter the need to group them or display them specifically according to certain criteria. Latte offers several powerful tools for this purpose.
The filter and function |group
allow for efficient data grouping based on specified criteria, the
|batch
filter facilitates splitting data into fixed-size batches, and the {iterateWhile}
tag provides
the ability to control loop progression with more complex conditions. Each of these features offers specific options for working
with data, making them indispensable tools for dynamic and structured display of information in Latte templates.
Filter and Function group
Imagine a database table items
with items divided into categories:
id | categoryId | name |
---|---|---|
1 | 1 | Apple |
2 | 1 | Banana |
3 | 2 | PHP |
4 | 3 | Green |
5 | 3 | Red |
6 | 3 | Blue |
A simple list of all items using a Latte template would look like this:
<ul>
{foreach $items as $item}
<li>{$item->name}</li>
{/foreach}
</ul>
However, if we wanted the items organized into groups by category, we need to divide them so that each category has its own list. The desired result would look like this:
<ul>
<li>Apple</li>
<li>Banana</li>
</ul>
<ul>
<li>PHP</li>
</ul>
<ul>
<li>Green</li>
<li>Red</li>
<li>Blue</li>
</ul>
This task can be easily and elegantly solved using |group
. We specify categoryId
as the parameter,
meaning the items will be divided into smaller arrays based on the value of $item->categoryId
(if
$item
were an array, $item['categoryId']
would be used):
{foreach ($items|group: categoryId) as $categoryId => $categoryItems}
<ul>
{foreach $categoryItems as $item}
<li>{$item->name}</li>
{/foreach}
</ul>
{/foreach}
The filter can also be used as a function in Latte, providing an alternative syntax:
{foreach group($items, categoryId) ...}
.
If you want to group items based on more complex criteria, you can use a function in the filter parameter. For example, grouping items by the length of their name would look like this:
{foreach ($items|group: fn($item) => strlen($item->name)) as $items}
...
{/foreach}
It’s important to note that $categoryItems
is not a regular array, but an object that behaves like an iterator.
To access the first item in the group, you can use the first()
function.
This flexibility in data grouping makes group
an exceptionally useful tool for presenting data in Latte
templates.
Nested Loops
Let's imagine our database table has an additional column subcategoryId
, defining subcategories for each item. We
want to display each main category in a separate <ul>
list and each subcategory within that main category in a
separate nested <ol>
list:
{foreach ($items|group: categoryId) as $categoryItems}
<ul>
{foreach ($categoryItems|group: subcategoryId) as $subcategoryItems}
<ol>
{foreach $subcategoryItems as $item}
<li>{$item->name}
{/foreach}
</ol>
{/foreach}
</ul>
{/foreach}
Integration with Nette Database
Let's demonstrate how to effectively use data grouping in combination with Nette Database. Assume we are working with the
items
table from the introductory example, connected via the categoryId
column to this
categories
table:
categoryId | name |
---|---|
1 | Fruits |
2 | Languages |
3 | Colors |
We load data from the items
table using Nette Database Explorer with the command
$items = $db->table('items')
. While iterating over this data, we can access not only attributes like
$item->name
and $item->categoryId
, but thanks to the relationship with the categories
table, also the related row via $item->category
. This relationship allows for interesting applications:
{foreach ($items|group: category) as $category => $categoryItems}
<h1>{$category->name}</h1>
<ul>
{foreach $categoryItems as $item}
<li>{$item->name}</li>
{/foreach}
</ul>
{/foreach}
In this case, we use the |group
filter to group by the related row object $item->category
, not
just the categoryId
column. As a result, the key variable $category
directly holds the
ActiveRow
object for that category, allowing us to display its name directly using {$category->name}
.
This is a practical example of how grouping can simplify templates and facilitate working with related data.
Filter |batch
The |batch
filter allows you to divide a list of items into groups (batches) with a predetermined number of items.
This filter is ideal for situations where you want to present data in several smaller chunks, for example, for better clarity or
visual layout on the page.
Imagine we have a list of items and want to display them in lists, where each list contains a maximum of three items. Using the
|batch
filter is very practical in such a case:
<ul>
{foreach ($items|batch: 3) as $batch}
{foreach $batch as $item}
<li>{$item->name}</li>
{/foreach}
{/foreach}
</ul>
In this example, the $items
list is divided into smaller groups, where each group ($batch
) contains
up to three items. Each batch is then displayed in a separate <ul>
list.
If the last group does not contain enough elements to reach the desired number, the second parameter of the filter allows you to define what this group will be supplemented with. This is ideal for aesthetically aligning elements where an incomplete row might look disordered.
{foreach ($items|batch: 3, '—') as $batch}
...
{/foreach}
Tag {iterateWhile}
We will demonstrate the same tasks addressed with the |group
filter using the {iterateWhile}
tag. The
main difference between the two approaches is that |group
first processes and groups all input data, whereas
{iterateWhile}
controls the loop's progression based on conditions, allowing iteration to proceed sequentially.
First, let's render the table with categories using iterateWhile
:
{foreach $items as $item}
<ul>
{iterateWhile}
<li>{$item->name}</li>
{/iterateWhile $item->categoryId === $iterator->nextValue?->categoryId}
</ul>
{/foreach}
While {foreach}
marks the outer part of the cycle, i.e., drawing lists for each category, the
{iterateWhile}
tag marks the inner part, i.e., individual items. The condition in the end tag says that repetition
will continue as long as the current and next element belong to the same category ($iterator->nextValue
is the next item).
If the condition were always true, all elements would be rendered within the first <ul>
:
{foreach $items as $item}
<ul>
{iterateWhile}
<li>{$item->name}
{/iterateWhile true}
</ul>
{/foreach}
The result would look like this:
<ul>
<li>Apple</li>
<li>Banana</li>
<li>PHP</li>
<li>Green</li>
<li>Red</li>
<li>Blue</li>
</ul>
What's the benefit of using iterateWhile
like this? If the $items
array is empty, no empty
<ul></ul>
tags will be printed.
If we specify the condition in the opening {iterateWhile}
tag, the behavior changes: the condition (and transition
to the next element) is performed at the beginning of the inner cycle, not at the end. Thus, while you always enter
{iterateWhile}
without conditions, you enter {iterateWhile $cond}
only when the condition
$cond
is met. And at the same time, the next element is written into $item
.
This is useful, for instance, when you want to render the first item in each category differently, like this:
<h1>Apple</h1>
<ul>
<li>Banana</li>
</ul>
<h1>PHP</h1>
<ul>
</ul>
<h1>Green</h1>
<ul>
<li>Red</li>
<li>Blue</li>
</ul>
We modify the original code to first render the item as a heading, and then use the inner {iterateWhile}
loop to
render subsequent items from the same category as list items:
{foreach $items as $item}
<h1>{$item->name}</h1>
<ul>
{iterateWhile $item->categoryId === $iterator->nextValue?->categoryId}
<li>{$item->name}</li>
{/iterateWhile}
</ul>
{/foreach}
Within a single {foreach}
loop, you can create multiple inner {iterateWhile}
loops and even nest
them. This could be used, for example, to group subcategories.
Let's assume the table has another column subcategoryId
, and besides having each category in a separate
<ul>
, each subcategory should be in a separate <ol>
:
{foreach $items as $item}
<ul>
{iterateWhile}
<ol>
{iterateWhile}
<li>{$item->name}
{/iterateWhile $item->subcategoryId === $iterator->nextValue?->subcategoryId}
</ol>
{/iterateWhile $item->categoryId === $iterator->nextValue?->categoryId}
</ul>
{/foreach}