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. For this purpose, Latte offers several powerful tools.

The filter and function |group allow for efficient data grouping based on specified criteria, while the |batch filter facilitates splitting data into fixed batches and the {iterateWhile} tag provides the possibility of more complex cycle control with conditions. Each of these tags 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 to be organized into groups by category, we need to divide them so that each category has its own list. The result would then 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>

The task can be easily and elegantly solved using |group. We specify categoryId as the parameter, meaning that the items will be divided into smaller arrays based on the $item->categoryId value (if $item were an array, we would use $item['categoryId']):

{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, giving us an alternative syntax: {foreach group($items, categoryId) ...}.

If you want to group items according to 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 say we have a database table with another column subcategoryId that defines subcategories for each item. We want to display each main category in a separate <ul> list and each subcategory 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}

Connection with Nette Database

Let's show how to effectively use data grouping in combination with Nette Database. Suppose we are working with the items table from the initial example, which is connected through the categoryId column to this categories table:

categoryId name
1 Fruits
2 Languages
3 Colors

We load data from the items table using the Nette Database Explorer command $items = $db->table('items'). During the iteration over these data, we have the opportunity not only to access attributes like $item->name and $item->categoryId, but thanks to the connection with the categories table, also to the related row in it via $item->category. This connection can demonstrate interesting uses:

{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 connected row $item->category, not just by the categoryId column. This gives us the ActiveRow of the given category in the variable key, allowing us to directly display its name using {$category->name}. This is a practical example of how grouping can simplify templates and facilitate data handling.

Filter |batch

The filter allows you to split a list of elements into groups with a predetermined number of elements. This filter is ideal for situations where you want to present data in several smaller groups, for example, for better clarity or visual organization on the page.

Imagine we have a list of items and want to display them in lists, each containing 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 list $items is divided into smaller groups, each group ($batch) containing up to three items. Each group 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 we 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, while {iterateWhile} controls the progress of cycles with conditions, so the iteration occurs sequentially.

First, we draw a 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 next item).

If the condition were always met, all elements would be drawn in the inner cycle:

{foreach $items as $item}
	<ul>
		{iterateWhile}
			<li>{$item->name}
		{/iterateWhile true}
	</ul>
{/foreach}

The result will look like this:

<ul>
	<li>Apple</li>
	<li>Banana</li>
	<li>PHP</li>
	<li>Green</li>
	<li>Red</li>
	<li>Blue</li>
</ul>

What is the use of iterateWhile in this way? When the table is empty and contains no elements, no empty <ul></ul> is 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 example, in a situation where we want to render the first element 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 so that we first render the first item and then in the inner cycle {iterateWhile} we render the other items from the same category:

{foreach $items as $item}
	<h1>{$item->name}</h1>
	<ul>
		{iterateWhile $item->categoryId === $iterator->nextValue->categoryId}
			<li>{$item->name}</li>
		{/iterateWhile}
	</ul>
{/foreach}

Within one cycle, we can create multiple inner loops and even nest them. This way, subcategories could be grouped, for example.

Suppose the table has another column subcategoryId, and besides each category being in a separate <ul>, each subcategory 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}
version: 3.0